Why 0.1 + 0.2 is not 0.3
Every programmer meets this one eventually. You open a console, add two of the simplest numbers there are, and the machine answers with a number that is wrong in the seventeenth place.
> 0.1 + 0.2
0.30000000000000004
> 0.1 + 0.2 === 0.3
falseIt looks like a bug in addition. It is not. The addition is exact to the last bit. The error was already there before the plus sign ran, because neither 0.1 nor 0.2 can be written in binary. What follows is where the extra 4 comes from, digit by digit, and what to do about it.
A decimal fraction rarely fits in binary
In decimal, a third has no finite form. You write 0.333… and stop somewhere, and the number you stored is a little less than a third. Binary has the same problem with different numbers. A fraction ends in binary only when its denominator is a power of two, and a tenth is not one of those. In binary, 0.1 is 0.0001100110011… with the 0011 repeating forever.
- 0.5, 0.25, and 0.375 are exact, because they are halves, quarters, and eighths.
- 0.1, 0.2, and 0.3 are not, because they are tenths.
- Neither is 0.7, 1.1, or almost any price you have ever typed into a form.
What the computer stores instead
JavaScript numbers, and doubles in nearly every other language, follow the IEEE 754 double format: 64 bits split into a sign, an 11 bit exponent, and a 52 bit fraction. The repeating pattern has to be cut off at bit 52 and rounded, and this is what 0.1 becomes.
sign exponent fraction
0 01111111011 1001100110011001100110011001100110011001100110011010Read back as a decimal, that bit pattern is not a tenth. It is the nearest double to a tenth, which is a slightly different number. The same is true for the other two.
| You write | What is stored | Off by |
|---|---|---|
0.1 | 0.10000000000000000555… | +5.55 × 10⁻¹⁸ |
0.2 | 0.20000000000000001110… | +1.11 × 10⁻¹⁷ |
0.3 | 0.29999999999999998890… | −1.11 × 10⁻¹⁷ |
Notice the signs. The nearest double to 0.1 sits above it, and so does the nearest double to 0.2. The nearest double to 0.3 sits below it. That asymmetry is the whole story.
The sum lands on a tie
Add the two stored values exactly and you get 0.30000000000000001665…, which is not a double either. It has to be rounded to one of its two neighbours, 0.29999999999999998890… below or 0.30000000000000004441… above. Measure the distance to each and they are exactly equal. The sum is a perfect tie.
IEEE 754 breaks ties by choosing the neighbour whose last bit is even, and that is the upper one. So the sum becomes 0.30000000000000004441…, while the literal 0.3 you typed on the other side of the comparison became the lower one. Two different doubles, so === says false, and it is right to.
Why 0.1 still prints as 0.1
If 0.1 is stored as 0.10000000000000000555…, why does the console show 0.1? Because printing does not show the stored value. It shows the shortest decimal that rounds back to the same double. For the double nearest a tenth, that shortest decimal is 0.1. For the sum, 0.3 would round back to the other neighbour, so the printer has to keep going until the digits identify the right one, and that takes seventeen of them.
Where it bites
One rounding error of 10⁻¹⁷ never matters on its own. It matters when it is compared, accumulated, or multiplied into money.
- Equality checks. Any test of the form
a + b === con fractions is a coin flip. - Accumulation. Adding 0.1 ten times does not give 1, and the error grows with every step.
- Money. Prices are decimal by law and by habit, and a cent that drifts is a bug a customer can see.
- Big integers. The 52 bit fraction also caps exact integers at 2⁵³. Above that, adding 1 can do nothing.
let total = 0;
for (let i = 0; i < 10; i++) {
total += 0.1;
}
total; // 0.9999999999999999
9007199254740992 + 1; // 9007199254740992What to do instead
The fix depends on what the number is for. Doubles are the right tool for measurements, geometry, and anything physical. They are the wrong tool for anything that has to come out to the cent.
- Count in the smallest unit. Store cents, not dollars, as integers. Every integer below 2⁵³ is exact, and so is every sum of them.
- Round for display, never for arithmetic.
toFixedandIntl.NumberFormatproduce strings for people to read. Do the maths before, on exact values. - Compare with a tolerance. Two doubles are equal when their difference is small relative to their size.
Number.EPSILONis the gap between 1 and the next double, so it only works for numbers near 1. Scale it. - Use a decimal type when one exists. Python has
decimal, Java hasBigDecimal, and JavaScript has a decimal proposal on the way.
const cents = 10 + 20; // 30, exact
cents / 100; // 0.3, for display only
(0.1 + 0.2).toFixed(2); // "0.30"
const close = (a, b, tolerance = 1e-9) =>
Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
close(0.1 + 0.2, 0.3); // trueThe thing to remember is where the rounding happened. Not in the addition, which was exact, but in the act of writing 0.1 down at all. Once you see the number as already rounded, every result that follows stops being surprising. For the long version, with every language’s output side by side, The Floating-Point Guide is the reference.