If you’ve written any kind of financial or precision-sensitive logic in JavaScript, you’ve likely hit this:
0.1 + 0.2 === 0.3 // false
0.1 + 0.2 // 0.30000000000000004
It looks like a bug. It isn’t. It’s a consequence of how JavaScript represents numbers — and understanding why it happens is the first step to writing code that doesn’t silently produce wrong results.
Why JavaScript Numbers Are Imprecise
JavaScript uses the IEEE 754 double-precision floating point format for all numbers. This means every number is stored in 64 bits: 1 for sign, 11 for exponent, and 52 for the fractional part (the mantissa).
The problem is that not all decimal fractions can be represented exactly in binary. 0.1 in base 10 is an infinitely repeating fraction in base 2 — similar to how 1/3 is 0.333... in base 10. The CPU has to truncate it somewhere, and that’s where the error creeps in.
This isn’t a JavaScript quirk. Java, Python, Ruby, and most modern languages share the same behavior. The difference is that JavaScript doesn’t have a native Decimal or BigDecimal type in its standard library, which makes the problem more visible.
Where This Actually Bites You
The raw 0.1 + 0.2 case looks obvious. The real danger is subtler — values that look fine in isolation but drift when composed:
const price = 8.5; const fee = price * 0.2;
console.log(fee); // 1.7000000000000002 console.log(fee === 1.7); // false
Or when you’re rounding and the floating point artifact crosses a threshold:
Math.ceil(1.7000000000000002 * 100) / 100 // 1.71, not 1.70
That last example is the kind of bug that makes it into production. The calculation looks correct, the individual values look correct, but the composed result is wrong by one cent.
Solution 1: toFixed() for Display, Not Logic
A common first instinct is toFixed():
(0.1 + 0.2).toFixed(2) // "0.30"
This works for display, but it returns a string. Using it in further calculations just pushes the problem forward:
parseFloat((0.1 + 0.2).toFixed(2)) + 0.01 // 0.31 — fine here, but fragile
Reserve toFixed() for formatting output. Don’t rely on it for intermediate computation.
Solution 2: Integer Arithmetic (Work in Cents)
The most reliable approach for money is to work in the smallest unit — cents — using integers.
const priceInCents = 850; // 8.50 AED const feeInCents = Math.ceil(priceInCents * 0.2); // 170 → 1.70 AED
const display = feeInCents / 100; // back to AED for output
Since integers are represented exactly in IEEE 754 (up to 2^53 - 1), you eliminate rounding errors in the critical path. Convert to the lowest denomination on input, do all math in integers, convert back on output.
The tradeoff: you need to be disciplined about when conversion happens. A single stray division that produces a float and re-enters your pipeline will reintroduce the problem.
Solution 3: Scale Up, Round Out the Noise, Then Ceiling
Sometimes you’re working with values that were already computed as floats — maybe from a third-party API or a formula you don’t fully control. In those cases, the trick is to neutralise the floating point noise before applying any rounding direction.
Here’s a pattern I use for ceiling-to-2-decimal-places:
function ceilTo2(value) { return Math.ceil(Math.round(value * 10000) / 100) / 100; }
ceilTo2(1.7000000000000002) // 1.70 (not 1.71) ceilTo2(10.011) // 10.02 ceilTo2(10.015) // 10.02 ceilTo2(10.01) // 10.01
What’s happening here:
value * 10000— scale up to eliminate sub-cent precision loss.1.7000000000000002 * 10000 = 17000.000000000002.Math.round(...)— snap to the nearest integer. The floating point error (2e-13) is far smaller than0.5, so17000.000000000002rounds cleanly to17000. A genuine third decimal digit like10.011 * 10000 = 100110.0stays at100110./ 100— back to cents-level precision.17000 / 100 = 170,100110 / 100 = 1001.1.Math.ceil(...)— ceiling on the cents value.170stays170.1001.1becomes1002./ 100— final conversion back to the display unit.
The key insight: floating point noise lives at around 1e-13 scale; real sub-cent values live at 0.001 or larger. The Math.round(value * 10000) step cleanly separates them.
Solution 4: decimal.js or big.js for Complex Use Cases
If your domain involves a lot of chained calculations — tax on top of discount on top of platform fee — arbitrary precision libraries remove the mental overhead entirely:
import Decimal from 'decimal.js';
const price = new Decimal('8.5'); const fee = price.times('0.2');
fee.toString() // "1.7" fee.equals(new Decimal('1.7')) // true
The tradeoff is bundle size and the cognitive overhead of using a wrapper type throughout your codebase. For pure backend work or complex financial logic, it’s often worth it. For a single rounding function, it’s overkill.
Which Approach Should You Use?
| Scenario | Recommended approach |
|---|---|
| Formatting a number for display | toFixed() |
| Storing and computing monetary values | Integer arithmetic (cents) |
| Rounding float outputs from formulas | Scale-round-ceil pattern |
| Complex chained financial calculations | decimal.js / big.js |
My default is integer arithmetic for anything money-related. It’s the most boring and the most correct. The scale-round-ceil pattern is a pragmatic fallback when you’re dealing with computed floats you can’t avoid.
The Takeaway
Floating point errors in JavaScript aren’t random or unpredictable — they’re deterministic artifacts of a well-defined standard. Once you understand that, the solutions become obvious: either avoid floats in the critical path entirely, or account for the error explicitly when you have to work with them.
The danger isn’t the 0.1 + 0.2 case that every developer has seen. It’s the Math.ceil(1.7000000000000002 * 100) / 100 = 1.71 case that slips through code review and ends up charging a customer one extra cent.


Leave a comment