Math·Numbers
Why 0.1 + 0.2 is not 0.3
Every language gets this answer, and it is not a bug. Binary floating point cannot hold 0.1, so the number you typed was never the number that got stored. Here is what is actually in there, and what to do about money.
Before you start
- A REPL in any language, to check the numbers here yourself
Type 0.1 + 0.2 into any language that uses IEEE 754 doubles, which is nearly all of them, and you get 0.30000000000000004. This surprises people once and then annoys them for years.
It is not a rounding bug in the addition. The addition was exact. The problem happened earlier, when 0.1 was stored.
The numbers you typed are not the numbers you have #
A double holds a number as a sign, a 52-bit fraction and an exponent, all in base two. That means it can only represent numbers that are some whole number divided by a power of two. One tenth is not one of those, in the same way one third has no exact decimal expansion.
So 0.1 gets stored as the closest double there is, and the closest double is not one tenth:
you typed 0.1
you stored 0.1000000000000000055511151231257827021181583404541015625
you typed 0.2
you stored 0.2000000000000000111022302462515654042363166809082031250
Add those two stored values and you get their exact sum, which lands nearer to a different double than the one closest to 0.3:
0.1 + 0.2 0.3000000000000000444089209850062616169452667236328125
0.3 0.2999999999999999888977697537484345957636833190917968750
Two different doubles, so === is false. The gap between them:
5.55e-17
0.1 + 0.2 − 0.3
Not zero, so === is false
15–17
Exact digits in a double
Decimal digits, not binary
9,
Largest exact integer
2⁵³ − 1
Computed with IEEE 754 binary64, the double type in JavaScript, Python floats, Java double and C double.
Note
Nothing here is specific to JavaScript. Python prints
0.30000000000000004too, and so do Java, C, Go, Ruby, Swift and PHP. Languages that appear to get it right, like some calculator apps, are rounding the output for display.
Why the error moves around #
The error is not a constant you can subtract. Each operation rounds to the nearest representable double, so errors accumulate in whichever direction the rounding happened to go.
Adding 0.1 to a running total, over and over, drifts in both directions before settling into a consistent bias:
Measured, not modelled: each point is the observed value of a loop adding 0.1 that many times, minus the exact decimal answer.
View data as a table
| Additions | Error |
|---|---|
| 10 | -0.111 |
| 20 | 0.444 |
| 30 | 1.33 |
| 40 | 1.78 |
| 50 | -1.78 |
| 60 | -5.33 |
| 70 | -8.88 |
| 80 | -12.43 |
| 90 | -15.99 |
| 100 | -19.54 |
Ten additions of 0.1 do not give you 1. They give you 0.9999999999999999. Which is why this test never passes:
let total = 0;
for (let i = 0; i < 10; i++) total += 0.1;
total === 1; // false
total; // 0.9999999999999999
The fix is not a better loop. It is to stop comparing floats for equality.
Precision runs out as numbers get bigger #
A double has a fixed number of significant digits, not a fixed number of decimal places. Spend them on the integer part and there are fewer left for the fraction.
At one quadrillion the gap between neighbouring doubles is larger than 1, so consecutive integers collide:
9007199254740992 === 9007199254740993; // true. Both are 2⁵³.
That is the real reason a 64-bit database id should not travel through JSON as a number. It arrives as a double, and ids past 2⁵³ quietly change value. Send them as strings.
What to do instead #
Three situations, three different answers. Picking by situation matters, because the standard advice for one of them is actively wrong for another.
Comparing two computed floats #
Compare with a tolerance, not with ===. And make the tolerance relative to the size of the numbers, because as the section above shows, an absolute epsilon that is right at 1 is far too small at a million.
const closeEnough = (a, b, tolerance = 1e-9) =>
Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
closeEnough(0.1 + 0.2, 0.3); // true
Money #
Do not use floats at all. Not with a tolerance, not with rounding at the end, not with a careful order of operations.
Store money as an integer number of the smallest unit, cents or minor units, and divide only when you display it. An invoice total that is out by 0.00000000000001 is not nearly right, it is a number that fails reconciliation and cannot be explained to an accountant.
// Cents, as integers. Exact, and it sorts and sums correctly.
const items = [1299, 499, 2350];
const subtotal = items.reduce((a, b) => a + b, 0); // 4148
const withVat = Math.round(subtotal * 1.2); // 4978
const display = (cents) => `€${(cents / 100).toFixed(2)}`;
display(withVat); // "€49.78"
Watch out
Rounding at the end does not save you, because the value you are rounding is already wrong in a direction you did not choose.
Math.round(1.005 * 100) / 100gives1, not1.01, because1.005is stored as1.00499999999999989...and it genuinely is below the midpoint. Every "round half up" helper written on top of floats has this bug.
Most languages ship an exact decimal type for exactly this: BigDecimal in Java, decimal.Decimal in Python, NUMERIC in Postgres. Use them, or use integers.
Displaying a result #
Round for output, and only for output. Keep full precision in the value and decide the number of digits at the edge, where you know what the number means.
const value = 0.1 + 0.2;
value.toFixed(2); // "0.30"
value.toPrecision(3); // "0.300"
The short version #
0.1is not representable in binary, so the value stored was never one tenth.- The addition is exact; the inputs were already approximate.
- Error accumulates in both directions, so it cannot be corrected by subtracting a constant.
- Precision is a budget of significant digits, and large integers spend all of it.
- Never floats for money. Integer minor units, or a decimal type.
When a JSON payload comes back with a number that looks slightly wrong, or an id that changed in transit, the JSON formatter here will show you the value exactly as it parsed, which is usually enough to tell a display problem from a precision one. It runs in your browser, so nothing is uploaded.