12k
All articles

3 Pegadinhas do JavaScript Explicadas

Três armadilhas do JavaScript explicadas: ponto flutuante, checagens de NaN e await em loops, com as correções certas.

OpenReplay Team
OpenReplay Team
3 Pegadinhas do JavaScript Explicadas

0.1 + 0.2 não é igual a 0.3, NaN não é igual a si mesmo, e await dentro de um loop pode transformar uma página rápida em lenta — três comportamentos do JavaScript que parecem bugs, mas são, na verdade, a linguagem fazendo exatamente o que sua especificação determina. Este artigo explica o mecanismo por trás de cada um, não apenas a saída estranha no console, e apresenta a correção adequada para cada caso. Todos eles se manifestam como sintomas reais para o usuário final: um total errado por alguns centavos, uma validação que deixa dados inválidos passarem, uma tela que carrega lentamente sem motivo aparente. Entender o porquê é o que permite evitar os três em produção e respondê-los com precisão em uma entrevista.

Principais Conclusões

  • 0.1 + 0.2 retorna 0.30000000000000004 porque o padrão IEEE 754 armazena números em binário, e nem 0.1 nem 0.2 possuem uma representação binária finita exata, portanto cada um é arredondado antes de serem somados.
  • Para valores monetários, calcule em centavos inteiros em vez de reais em ponto flutuante; para comparações gerais de floats, teste Math.abs(a - b) < tolerância com uma tolerância dimensionada à magnitude dos seus números, não um Number.EPSILON aplicado indiscriminadamente.
  • NaN === NaN é false porque a especificação IEEE 754 define NaN como desigual a qualquer valor, incluindo ele mesmo — use Number.isNaN(), que não realiza coerção, em vez do isNaN() global.
  • await dentro de um loop for pausa o loop inteiro a cada iteração; inicie as promises primeiro e use await Promise.all(...) para executar requisições independentes de forma concorrente.

Pegadinha #1: Por que 0.1 + 0.2 não é 0.3 (aritmética de ponto flutuante)

0.1 + 0.2 retorna 0.30000000000000004 porque os números em JavaScript são floats de dupla precisão IEEE 754 armazenados em base 2, e nem 0.1 nem 0.2 possuem uma representação binária finita exata. Cada literal é arredondado para o valor de 64 bits representável mais próximo no momento em que é escrito, e a soma desses dois valores já arredondados resulta em um número ligeiramente acima de 0.3.

0.1 + 0.2;             // 0.30000000000000004
0.1 + 0.2 === 0.3;     // false
(0.1).toPrecision(20); // "0.10000000000000000555"

Essa última linha é reveladora: o valor armazenado para 0.1 nunca foi exatamente 0.1. Isso não é uma peculiaridade do JavaScript — é uma propriedade do ponto flutuante de dupla precisão compartilhada por Python, Java, C e qualquer linguagem que utilize o mesmo formato. O número 0.1 em binário é uma fração periódica, da mesma forma que 1/3 é 0,333… em decimal, portanto precisa ser truncado.

A correção depende do que você está calculando:

// Valores monetários: trabalhe em centavos inteiros, formate apenas para exibição
const total = 1010 + 2030;   // 3040 centavos
(total / 100).toFixed(2);    // "30.40"

// Comparação geral: tolerância dimensionada à sua magnitude
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true

Para valores monetários, armazene e calcule em centavos inteiros — nunca em reais com ponto flutuante — e então divida e aplique toFixed(2) apenas na camada de exibição. Para igualdade aproximada, compare contra uma tolerância. Number.EPSILON funciona apenas para números em torno da magnitude de 1 — o MDN adverte explicitamente que não é um limite seguro para uso indiscriminado, portanto dimensione a tolerância ao tamanho dos valores que você está comparando. Se você estiver somando um array, Math.sumPrecise() tornou-se Baseline recentemente disponível em abril de 2026, portanto pode não funcionar em dispositivos mais antigos. Vale notar também que ele ainda não consegue evitar o problema de precisão de 0.1 + 0.2 para literais individuais — ele apenas previne o acúmulo de erros em longas somatórias.

Um total errado por alguns centavos só se reproduz com a sequência exata de valores que foram somados, razão pela qual um session replay que reconstrói a ordem real das entradas é mais útil aqui do que uma captura de tela do número incorreto.

Pegadinha #2: NaN é o único valor que não é igual a si mesmo

NaN === NaN é false porque o padrão IEEE 754 define NaN (“Not-a-Number”) como desigual a qualquer valor, incluindo ele mesmo, e o JavaScript segue essa regra literalmente. Isso torna NaN o único valor na linguagem que não é igual a si mesmo — um fato que você pode até usar como detector de NaN.

NaN === NaN;   // false
NaN == NaN;    // false
typeof NaN;    // "number"

typeof NaN é 'number': NaN é um valor numérico que representa um resultado numérico indefinido ou irrepresentável, não um tipo separado. A armadilha real é o isNaN() global, que coerce seu argumento para um número antes de testar, fazendo com que valores que não são NaN sejam reportados como se tivessem sido verificados como números:

isNaN('');   // false  — '' é convertido para 0
isNaN([]);   // false  — [] é convertido para 0
isNaN('45'); // false  — '45' é convertido para 45
isNaN({});   // true   — {} é convertido para NaN

A correção é Number.isNaN(), adicionado no ES2015, que não realiza coerção e retorna true apenas para o valor NaN propriamente dito:

EntradaisNaN() (com coerção)Number.isNaN() (sem coerção)
NaNtruetrue
'NaN'truefalse
''falsefalse
[]falsefalse
'45'falsefalse
undefinedtruefalse

Use Number.isNaN() para verificar “este valor é especificamente NaN”, e Number.isFinite() quando quiser saber “este é um número real e finito” — ele rejeita NaN, Infinity e não-números sem coerção. Um mito relacionado que vale a pena desmentir: parseInt("032") retorna 32, não 26. A detecção automática de strings com zero à esquerda como octais foi removida no ECMAScript 5, portanto a afirmação amplamente copiada de que o resultado seria 26 é um resquício anterior a 2011 — ainda passe um radix, mas pelo motivo correto.

Quando uma validação falha porque isNaN('') converteu uma string vazia para 0, o valor problemático frequentemente aparece como “vazio” em um relatório de bug; reproduzir as teclas digitadas e o estado do campo mostra exatamente o que passou despercebido.

Pegadinha #3: await em um loop serializa requisições e torna sua UI lenta

await dentro de um loop for pausa o loop inteiro a cada iteração, fazendo com que requisições sem dependência entre si sejam executadas estritamente uma após a outra, em vez de em paralelo. Cada iteração aguarda a resolução de sua promise antes que a próxima requisição sequer comece, transformando N round-trips independentes em N sequenciais.

// Serial: cada await bloqueia a próxima iteração
async function getUsers(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // aguarda ~1,5s, toda vez
  }
  return users;
}

A correção é iniciar todas as promises primeiro e então aguardá-las juntas com Promise.all(), que dispara as requisições de forma concorrente e resolve quando todas forem concluídas:

async function getUsers(ids) {
  return Promise.all(ids.map(fetchUser));
}

Com uma latência fixa simulada de 1,5 segundo por requisição (ilustrando a serialização, não o tempo real de rede), a diferença escala com o tamanho do lote:

Abordagem3 requisições10 requisições
await em loop (serial)~4,5s~15s
Promise.all (concorrente)~1,5s~1,5s

Uma ressalva: Promise.all rejeita assim que qualquer promise individual rejeita, descartando as demais. Quando uma falha não deve abortar o lote inteiro, use Promise.allSettled(), que aguarda todas as promises independentemente do resultado e reporta cada uma como fulfilled ou rejected. Promise.allSettled foi lançado no ES2020 e está disponível como baseline em todos os navegadores desde o início de 2023, portanto não requer polyfill em ambientes modernos. Serialize deliberadamente apenas quando cada requisição depende genuinamente do resultado da anterior.

Uma tela que está “simplesmente lenta” para alguns usuários é difícil de identificar em uma revisão de código; um session replay evidencia requisições disparando uma após a outra em vez de simultaneamente, apontando diretamente para um loop com await.

Próximos Passos

Esses três comportamentos estão em conformidade com a especificação, e é exatamente por isso que sobrevivem à revisão de código e chegam aos usuários: floats binários arredondam porque o IEEE 754 determina isso, NaN se rejeita porque o padrão o define assim, e await serializa porque é isso que pausar a execução significa. As defesas são simples e mecânicas — centavos inteiros e tolerâncias dimensionadas à magnitude para floats, Number.isNaN() e Number.isFinite() para verificações numéricas, Promise.all ou Promise.allSettled para trabalho assíncrono independente. Audite seus próprios cálculos monetários, validações e loops de busca de dados com base nesses três padrões, e a categoria de bugs “impossíveis” que eles causam deixará de chegar em produção.

Perguntas Frequentes

Qual é a diferença entre o isNaN() global e o Number.isNaN()?

O isNaN() global coerce seu argumento para um número antes de testar, portanto isNaN('') e isNaN([]) retornam false porque '' e [] são convertidos para 0, enquanto isNaN('NaN') retorna true porque 'NaN' é convertido para o valor NaN. Number.isNaN(), adicionado no ES2015, não realiza coerção e retorna true apenas para o valor NaN propriamente dito. Use Number.isNaN() quando precisar especificamente detectar NaN.

O problema de ponto flutuante com 0.1 + 0.2 ocorre apenas no JavaScript?

Não. Ocorre em qualquer linguagem que utilize floats de dupla precisão IEEE 754, incluindo Python, Java, C, C++ e Ruby. Nem 0.1 nem 0.2 possuem uma representação binária finita exata, portanto cada um é arredondado quando armazenado, e a soma resulta em 0.30000000000000004. É uma propriedade do formato de ponto flutuante binário em si, não um bug do JavaScript, portanto as mesmas correções baseadas em centavos inteiros e tolerâncias se aplicam em todas as linguagens.

Quando devo usar Promise.allSettled em vez de Promise.all?

Use Promise.allSettled quando uma falha não deve abortar o lote inteiro. Promise.all rejeita assim que qualquer promise rejeita e descarta os resultados das demais, portanto é adequado para operações do tipo tudo-ou-nada. Promise.allSettled aguarda todas as promises independentemente do resultado e retorna um array reportando cada uma como fulfilled ou rejected, que é o que você deseja ao buscar múltiplos recursos independentes onde o sucesso parcial é aceitável. Foi lançado no ES2020 e não requer polyfill em ambientes modernos.

É correto usar await dentro de um loop em alguma situação?

Sim, quando cada iteração genuinamente depende do resultado da anterior, como ao paginar por uma API onde a próxima requisição precisa de um cursor retornado pela última resposta, ou ao limitar deliberadamente a taxa de requisições para não sobrecarregar um servidor. Nesses casos, a execução serial é o comportamento desejado. A pegadinha se aplica apenas a requisições independentes que não possuem dependência entre si, onde o await em loop serializa desnecessariamente um trabalho que Promise.all poderia executar de forma concorrente.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.