0.1 + 0.2 Made Your Invoice Wrong: Money Math in JavaScript
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, butNumber(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.
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.