Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori7 min de leitura

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.

js
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);
number number bigint true false true 3.5 Infinity -Infinity NaN

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

js
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));
0.30000000000000004 false 0.30000000000000004441 0.10000000000000000555

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:

js
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));
19.999999999999996 false 20.00

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:

js
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);
false 3.552713678800501e-15

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:

js
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 string 289.9010 299.9 299.9

'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:

js
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));
1.00 2.67 8.35 1.01 1.04

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:

js
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));
}
2.5 3 2 3 2 -2.5 -2 -3 -2 -2 2.4 2 2 3 2 -2.4 -2 -3 -2 -2 2.6 3 2 3 2 -2.6 -3 -3 -2 -2

Duas coisas para guardar dessa tabela:

  • Math.round(-2.5) é -2, não -3. O critério do round é “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) é -3 e Math.trunc(-2.4) é -2. floor desce, trunc corta. Para calcular quantas caixas fechadas cabem num pedido, você quer floor; 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:

js
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));
20 1 2.68 289.94

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:

js
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);
2000 true 20

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:

js
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)));
[ 3334, 3333, 3333 ] 10000 [ '33.34', '33.33', '33.33' ]

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:

js
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));
9007199254740991 9007199254740992 9007199254740992 true false

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.

js
const resposta = '{"chaveNfe": 35240612345678901234550010000012341000012345}';
console.log(JSON.parse(resposta).chaveNfe);
3.5240612345678903e+43

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:

js
const chave = 9007199254740993n;
console.log(chave, chave + 1n);
console.log(typeof chave);
console.log(String(chave));
9007199254740993n 9007199254740994n bigint 9007199254740993

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:

js
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'));
NaN false true false false

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:

js
const total = 0.1 + 0.2;

console.log(total === 0.3);
console.log(Math.abs(total - 0.3) < Number.EPSILON);
console.log(Number.EPSILON);
false true 2.220446049250313e-16

Erros comuns

O primeiro é chamar toFixed num valor que veio do JSON como string:

js
const resposta = { id: 4821, total: '289.9' };

console.log(resposta.total.toFixed(2));
file:///private/tmp/loja/total-string.mjs:3 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:

js
const total = 289.9;

console.log(total.toFixed(120));
file:///private/tmp/loja/casas-demais.mjs:3 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:

js
const chaveNfe = 9007199254740993n;

console.log(chaveNfe + 1);
file:///private/tmp/loja/bigint-misto.mjs:3 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.

Ver todos os vídeos do canal
  • numeros
  • arredondamento
  • tofixed
  • ponto flutuante
  • dinheiro

Perguntas frequentes

Por que 0.1 + 0.2 não dá 0.3 em JavaScript?
Porque 0.1 e 0.2 não existem em binário com precisão exata, do mesmo jeito que um terço não existe em decimal com precisão exata. O que fica guardado é o número representável mais próximo, e a soma de duas aproximações aparece na décima sétima casa. Não é bug da linguagem: qualquer linguagem com double IEEE-754 faz igual.
Posso usar toFixed(2) para dinheiro?
Para exibir, sim, desde que você lembre que o retorno é string e não volte a somar com ele. Para calcular, não: toFixed arredonda a partir do valor binário já impreciso, e (1.005).toFixed(2) devolve "1.00". Cálculo de dinheiro se faz em centavos inteiros.
Qual a diferença entre Math.floor e Math.trunc?
Para número positivo, nenhuma. Para negativo, floor desce para o inteiro menor (-2.4 vira -3) e trunc simplesmente corta a parte decimal (-2.4 vira -2). Em desconto e estorno, a diferença é um real a mais ou a menos.
Quando preciso de BigInt?
Quando o inteiro passa de 9007199254740991, o MAX_SAFE_INTEGER. Chave de nota fiscal, id de banco de 64 bits e alguns identificadores de rede caem nesse caso. Só não dá para misturar BigInt e number na mesma conta — a linguagem lança TypeError em vez de arriscar um resultado errado.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Number — developer.mozilla.org
  2. MDN — Math — developer.mozilla.org
  3. IEEE 754-2019 — Standard for Floating-Point Arithmetic — standards.ieee.org

Continue por aqui