Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

0.1 + 0.2 Made Your Invoice Wrong: Money Math in JavaScript

C
Carlo Fontanos
· 2 min read

Every developer meets this eventually, usually via an angry email about a cart total:

0.1 + 0.2;              // 0.30000000000000004
19.99 * 3;              // 59.96999999999999
(1.005).toFixed(2);     // "1.00" - not "1.01"!

Binary floating point can't represent most decimal fractions exactly, the same way decimal can't write 1/3. Each price is stored as the nearest representable binary number, tiny errors compound through arithmetic, and toFixed rounds whatever the float actually is - 1.005 is really 1.00499999999999989..., so down it goes.

The fix that fintech uses: integer minor units

Integers up to Number.MAX_SAFE_INTEGER (9 quadrillion) are exact. So money lives as cents, and floats never touch the ledger:

const item = { name: 'Invoice Manager Pro', cents: 4900 };

const subtotal = items.reduce((sum, i) => sum + i.cents * i.qty, 0);   // exact
const tax      = Math.round(subtotal * 0.19);                          // round ONCE, deliberately
const total    = subtotal + tax;                                       // exact again

Multiplication by a rate produces a float momentarily - that's fine; you round once, at a business-defined boundary, and return to integer land. The bug pattern this kills is accumulated error: rounding at every intermediate step, or worse, never, then formatting whatever emerged.

Display is a separate concern - give it to Intl

const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
fmt.format(total / 100);   // "$59.97" - division for DISPLAY only

The only float division happens at the last moment, for rendering, where a 10⁻¹⁶ error can't survive the two-decimal formatting. Intl.NumberFormat handles symbols, separators and locales; your math never sees a formatted string.

The supporting cast

  • Parsing input: convert user-typed "19.99" straight to cents with string surgery, not parseFloat: Math.round(parseFloat(v) * 100) is mostly fine, but Number(v.replace('.', ''))-style digit handling avoids the float hop entirely for strict cases.
  • Comparing floats you didn't control: Math.abs(a - b) < Number.EPSILON * scale - but if you're epsilon-comparing money, the real bug is upstream.
  • Serialization: integer cents survive JSON perfectly; floats invite re-parse drift on other platforms. Your PHP backend should store cents too - same reasoning, same fix (or its BCMath/decimal extensions for rates).
  • BigInt exists for sums beyond 9 quadrillion cents; if that's you, hello, and also use a real decimal library.

None of this is JavaScript-specific weirdness - it's IEEE 754, the same in PHP, Python and Java. The languages differ only in how loudly they warn you. Integers for math, one deliberate rounding rule, Intl for pixels: invoices that add up, in any currency, forever.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading