Números em JavaScript: arredondar e casas decimais
Por que 0.1 + 0.2 não dá 0.3, por que toFixed devolve string e arredonda errado, e como somar dinheiro sem perder centavo — tudo executado no Node 24.
Em JavaScript não existe tipo inteiro. Todo número que você escreve — 2,
19.90, -1 — é um ponto flutuante de 64 bits, o double do padrão IEEE-754.
Essa única decisão explica quase tudo que parece estranho nas contas da
linguagem.
A regra curta, para você já sair aplicando: dinheiro se calcula em centavos
inteiros e se formata só na hora de mostrar. toFixed serve para exibir, não
para calcular, e devolve uma string. Arredondar com Math.round sobre o valor
já em centavos evita todo o resto.
O domínio dos exemplos é o carrinho de uma papelaria online — quatro itens baratos que deveriam somar exatamente R$ 20,00.
A régua binária por trás de todo número
Uma régua comum marca centímetros e milímetros. A régua do computador marca
frações formadas por potências de dois. Alguns valores decimais, como 0.1,
caem entre duas marcas dessa régua e precisam ser aproximados. O número que você
vê continua muito perto do esperado, mas a diferença reaparece quando a conta é
comparada ou formatada.
Esse é o comportamento do formato técnico IEEE-754 de dupla precisão, usado
pelo tipo number. Não é uma conta feita “errado” pelo JavaScript; é uma
representação finita. Quando o primeiro exemplo rodar, compare o resultado de
0.1 + 0.2 com 0.3: essa diferença verificável é o motivo de a gente guardar
dinheiro em centavos inteiros.
console.log(typeof 42, typeof 42.5, typeof 10n);
console.log(Number.isInteger(42), Number.isInteger(42.5));
console.log(42 === 42.0);
console.log(7 / 2);
console.log(10 / 0, -10 / 0, 0 / 0);42 e 42.0 são o mesmo valor. Number.isInteger não pergunta o tipo, e sim
se o valor não tem parte fracionária. E divisão por zero não lança exceção: ela
devolve Infinity, que segue circulando pelo programa como se fosse um número
normal.
Só existe um segundo tipo numérico, o bigint, e ele é opcional — volto nele no
fim.
0.1 + 0.2, e o carrinho que não fecha em R$ 20,00
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
console.log((0.1 + 0.2).toFixed(20));
console.log((0.1).toFixed(20));A quarta linha é a explicação: 0.1 sozinho já não é 0.1. Em binário, um
décimo é uma dízima infinita, do mesmo jeito que um terço é dízima em decimal. O
que fica na memória é o valor representável mais próximo, e a diferença aparece
quando você soma duas dessas aproximações.
Isso não é curiosidade acadêmica. É o carrinho:
const carrinho = [
{ nome: 'Caneta esferográfica azul', preco: 1.7 },
{ nome: 'Cola bastão 40g', preco: 6.9 },
{ nome: 'Caderno 96 folhas', preco: 8.7 },
{ nome: 'Borracha branca', preco: 2.7 },
];
const total = carrinho.reduce((soma, item) => soma + item.preco, 0);
console.log(total);
console.log(total === 20);
console.log(total.toFixed(2));Quatro preços que qualquer criança soma de cabeça, e o total é
19.999999999999996. Na tela, toFixed(2) esconde o problema. Na regra de
negócio, não:
const carrinho = [1.7, 6.9, 8.7, 2.7];
const total = carrinho.reduce((soma, preco) => soma + preco, 0);
const cupom = 20;
console.log(total >= cupom);
console.log(cupom - total);O cliente tem R$ 20,00 em cupom, o carrinho custa R$ 20,00, e o checkout diz que falta pagar três femtocentavos. Esse é o bug clássico de e-commerce em ponto flutuante — e ele não aparece em teste com dois itens.
toFixed devolve string, e arredonda diferente do que você espera
Duas surpresas em um método só. A primeira:
const total = 289.9;
const formatado = total.toFixed(2);
console.log(formatado, typeof formatado);
console.log(formatado + 10);
console.log(Number(formatado) + 10);
console.log(+formatado + 10);'289.90' + 10 concatena, porque o + com uma string de um dos lados é
concatenação. O resultado é 289.9010, um valor que passa despercebido num log
e vira desastre num total.
A segunda surpresa é o arredondamento:
console.log((1.005).toFixed(2));
console.log((2.675).toFixed(2));
console.log((8.345).toFixed(2));
console.log((1.015).toFixed(2));
console.log((1.045).toFixed(2));Cinco números terminados em 5, três arredondaram para baixo e dois para cima.
Não há regra decimal que explique isso — e não precisa haver, porque o
arredondamento não acontece em decimal. 1.005 guardado em binário é um
pouquinho menor que 1,005, então arredonda para 1.00. 8.345 é um
pouquinho maior, e sobe.
round, floor, ceil e trunc
Quatro formas de tirar a parte decimal, e elas só coincidem em número positivo:
for (const v of [2.5, -2.5, 2.4, -2.4, 2.6, -2.6]) {
console.log(v, Math.round(v), Math.floor(v), Math.ceil(v), Math.trunc(v));
}Duas coisas para guardar dessa tabela:
Math.round(-2.5)é-2, não-3. O critério doroundé “meio para cima” no sentido do eixo, ou seja, sempre em direção a+Infinity. Em estorno e desconto isso muda o valor devolvido ao cliente.Math.floor(-2.4)é-3eMath.trunc(-2.4)é-2.floordesce,trunccorta. Para calcular quantas caixas fechadas cabem num pedido, você querfloor; para descartar decimal de um saldo que pode ser negativo,trunc.
Arredondar em N casas sem depender de toFixed
O truque conhecido é multiplicar, arredondar e dividir. Funciona quase sempre — e vale saber onde ele também escorrega:
const arredondar = (n, casas = 2) => Math.round(n * 10 ** casas) / 10 ** casas;
console.log(arredondar(19.999999999999996));
console.log(arredondar(1.005));
console.log(arredondar(2.675));
console.log(arredondar(289.94499, 2));Repare: arredondar(1.005) devolveu 1, e (1.005).toFixed(2) devolveu
'1.00' — mesmo resultado, pelo mesmo motivo binário. Já 2.675 sobe para
2.68 aqui e desce para '2.67' no toFixed, porque a multiplicação por 100
introduz um erro diferente. Dois métodos, dois resultados, nenhum “certo”.
A conclusão prática não é escolher o melhor dos dois. É não deixar o valor chegar impreciso até aqui.
A saída de verdade: trabalhe em centavos
Se o menor valor que o seu domínio reconhece é um centavo, guarde centavos
inteiros. Inteiro até 9 quatrilhões é exato em double, então a aritmética
volta a ser aritmética:
const carrinho = [170, 690, 870, 270];
const totalCentavos = carrinho.reduce((soma, c) => soma + c, 0);
console.log(totalCentavos);
console.log(totalCentavos === 2000);
console.log(totalCentavos / 100);O mesmo carrinho que dava 19.999999999999996 agora fecha exato. A divisão por
100 só aparece na borda, na hora de mostrar.
Em centavos, dividir também passa a ser um problema resolvível em vez de um problema escondido. Repartir R$ 100,00 em três parcelas:
const totalCentavos = 10000;
const parcelas = 3;
const base = Math.floor(totalCentavos / parcelas);
const resto = totalCentavos - base * parcelas;
const valores = Array.from({ length: parcelas }, (_, i) => base + (i < resto ? 1 : 0));
console.log(valores);
console.log(valores.reduce((s, v) => s + v, 0));
console.log(valores.map((v) => (v / 100).toFixed(2)));O centavo que sobra vai para a primeira parcela, de propósito e por escrito. Com
toFixed em cada terço você teria três vezes 33.33 e um real perdido no
caminho — o tipo de diferença que o financeiro encontra e o desenvolvedor
demora um mês para reproduzir.
MAX_SAFE_INTEGER: onde o inteiro deixa de ser confiável
O double guarda 53 bits de mantissa. Passando disso, inteiros consecutivos
deixam de ter representação própria:
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MAX_SAFE_INTEGER + 1);
console.log(Number.MAX_SAFE_INTEGER + 2);
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);
console.log(Number.isSafeInteger(9007199254740993));Dois números diferentes que a linguagem considera iguais. No Brasil isso encosta em código real todo dia: a chave de acesso da NF-e tem 44 dígitos.
const resposta = '{"chaveNfe": 35240612345678901234550010000012341000012345}';
console.log(JSON.parse(resposta).chaveNfe);A chave foi destruída no JSON.parse, sem erro nenhum. A saída é pedir para a
API mandar identificador como string, ou usar BigInt:
const chave = 9007199254740993n;
console.log(chave, chave + 1n);
console.log(typeof chave);
console.log(String(chave));NaN, Infinity e a comparação que nunca dá certo
NaN é o resultado de uma conta que não produziu número. Ele é o único valor da
linguagem que não é igual a si mesmo:
const preco = Number('R$ 19,90');
console.log(preco);
console.log(preco === NaN);
console.log(Number.isNaN(preco));
console.log(isNaN('20'), Number.isNaN('20'));Nunca compare com === NaN: sempre Number.isNaN. E prefira Number.isNaN ao
isNaN global, que converte o argumento antes de testar e por isso responde
false para a string '20' — quase certo por acaso, e errado quando você
menos espera.
Para comparar dois decimais que deveriam ser iguais, compare com tolerância:
const total = 0.1 + 0.2;
console.log(total === 0.3);
console.log(Math.abs(total - 0.3) < Number.EPSILON);
console.log(Number.EPSILON);Erros comuns
O primeiro é chamar toFixed num valor que veio do JSON como string:
const resposta = { id: 4821, total: '289.9' };
console.log(resposta.total.toFixed(2));TypeError: resposta.total.toFixed is not a function at file:///private/tmp/loja/total-string.mjs:3:28 Node.js v24.16.0
toFixed é método de número. Se o valor chegou com aspas, converta com
Number(resposta.total) antes — e trate o caso de a conversão dar NaN, que é
o assunto de formatar moeda em JavaScript.
O segundo é pedir casas demais:
const total = 289.9;
console.log(total.toFixed(120));RangeError: toFixed() digits argument must be between 0 and 100 at Number.toFixed (<anonymous>) at file:///private/tmp/loja/casas-demais.mjs:3:19 Node.js v24.16.0
Parece um erro artificial, mas ele aparece quando o número de casas vem de configuração e alguém salvou o campo errado.
O terceiro é misturar BigInt com number na mesma conta:
const chaveNfe = 9007199254740993n;
console.log(chaveNfe + 1);TypeError: Cannot mix BigInt and other types, use explicit conversions at file:///private/tmp/loja/bigint-misto.mjs:3:22 Node.js v24.16.0
A linguagem se recusa a adivinhar. Escreva 1n, ou converta com Number(chave)
aceitando a perda de precisão — mas escolha conscientemente.
Regras práticas
| situação | o que fazer | por quê |
|---|---|---|
| somar preços | inteiros em centavos | 1.7 + 6.9 + 8.7 + 2.7 não dá 20 |
| exibir um total | toFixed(2) ou Intl.NumberFormat |
só na borda, nunca no meio do cálculo |
| comparar decimais | Math.abs(a - b) < Number.EPSILON |
=== compara bits, não valores “próximos” |
| cortar decimal de negativo | Math.trunc |
Math.floor(-2.4) é -3 |
| conferir se é número | Number.isNaN e Number.isFinite |
as versões globais convertem antes |
| id grande de API | pedir como string, ou BigInt |
acima de 2⁵³ o double inventa vizinhos |
O passo seguinte é levar esses centavos para a tela em português: vírgula decimal, ponto de milhar e o cifrão no lugar certo. É o assunto de formatar moeda em JavaScript. Se os valores da sua tela ainda chegam como texto, revise antes os métodos de string. E o guia completo de JavaScript mostra onde esta lição entra no roteiro.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Por que 0.1 + 0.2 não dá 0.3 em JavaScript?
Posso usar toFixed(2) para dinheiro?
Qual a diferença entre Math.floor e Math.trunc?
Quando preciso de BigInt?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Number — developer.mozilla.org
- MDN — Math — developer.mozilla.org
- IEEE 754-2019 — Standard for Floating-Point Arithmetic — standards.ieee.org



