Why 0.1 + 0.2 Is Not 0.2 + 0.1's Problem
The famous result is not a bug and not a rounding display quirk. 0.1 has no exact binary representation for the same reason 1/3 has no exact decimal one, and every consequence — failed equality tests, drifting sums, order-dependent results — follows from that single fact.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
The number was wrong before the addition happened
In decimal, 1/3 cannot be written exactly with finitely many digits — 0.333… repeats forever, so any stored version is truncated. Binary has the same limitation with a different set of victims. A fraction is exactly representable in binary only if its denominator is a power of two, so 0.5, 0.25 and 0.75 are exact while 0.1, 0.2 and 0.3 are not: in binary, 0.1 is 0.0001100110011… repeating.
So when source says 0.1, what is stored is the nearest double to 0.1, which is very slightly larger. Same for 0.2. The FPU then adds those two stored values with complete accuracy and rounds the exact sum to the nearest double — which happens to be very slightly above the nearest double to 0.3. Three correct roundings produce an inequality.
The clinching detail is that 0.1 + 0.2 === 0.3 is false while 0.5 + 0.25 === 0.75 is true, on the same hardware, with the same operations. Nothing about the arithmetic differs; only whether the inputs were representable.
Written Nearest double (20 significant digits)
0.1 0.10000000000000000555111512312578
0.2 0.20000000000000001110223024625157
0.3 0.29999999999999998889776975374843
0.1 + 0.2 -> 0.30000000000000004440892098500626
0.3 -> 0.29999999999999998889776975374843
^ different values, so === is false
Exactly representable, because the denominators are powers of two:
0.5 = 2^-1 exact
0.25 = 2^-2 exact
0.75 = 2^-1 + 2^-2 exact
0.5 + 0.25 === 0.75 true
0.1 in binary is 0.0001100110011001100... repeating forever,
the same way 1/3 is 0.333... forever in decimal.Comparison: absolute tolerance is not enough
The standard advice is to compare with a tolerance rather than for equality, and it is right as far as it goes. The trap is choosing a fixed absolute tolerance, because as Floating Point: Trading Precision for Range shows, the spacing between representable values grows with magnitude. A tolerance of 1e-9 is far too loose near 1e-15 and far too tight near 1e9, where consecutive doubles may be further apart than that.
The more robust form is a relative tolerance, scaled by the magnitude of the values being compared, usually combined with a small absolute floor so that comparisons against zero still work — a relative tolerance is meaningless when one side is zero.
The honest caveat is that any tolerance encodes a judgement about how much error your computation is expected to accumulate, and there is no universal value. Getting that judgement right requires knowing roughly how many operations contributed, which is why the summation question below matters.
1if (a === b) { /* essentially never true for computed floats */ }2 3const EPS = 1e-94if (Math.abs(a - b) < EPS) { /* ... */ }5// Near 1e-15: absurdly loose -- treats unrelated values as equal.6// Near 1e9: absurdly tight -- adjacent doubles differ by more7// than 1e-9 up there, so it can never be true.1function closeEnough(a: number, b: number,2 rel = 1e-9, abs = 1e-12): boolean {3 const diff = Math.abs(a - b)4 if (diff <= abs) return true // handles the zero case5 return diff <= rel * Math.max(Math.abs(a), Math.abs(b))6}7// Scales with magnitude, so it behaves sensibly at 1e-15 and 1e98// alike. 'rel' still encodes how much accumulated error you expect,9// which is a judgement about your computation, not a constant.The absolute-epsilon version is testing a fixed distance against a spacing that varies over roughly thirty orders of magnitude. The relative version compares like with like. Neither makes the arithmetic exact — they choose a threshold below which you have decided the difference does not matter.
Accumulation, and why addition is not associative
Each operation rounds, so errors accumulate across a long computation. The effect is worst when values of very different magnitudes are combined: adding a tiny value to a large running total can be a complete no-op, because the exact result rounds straight back to the total. Sum a million small numbers into a large accumulator and a substantial fraction of them may contribute nothing at all.
This is why floating-point addition is not associative: (a + b) + c and a + (b + c) can genuinely differ. That has a direct practical consequence for parallelism — a parallel reduction regroups the additions by construction, so a multi-threaded sum can produce a different result from the sequential one, and different results run to run as the thread scheduling varies. That is not a bug in the reduction; it is the format.
The mitigations are ordinary engineering. Sorting smallest-first keeps the accumulator near the magnitude of what is being added. Compensated summation tracks the lost low-order bits in a second variable and folds them back in, recovering most of the accuracy for a modest constant cost. Using a wider accumulator than the data is often the simplest fix of all.
1const values = [1e16, 1.0, 1.0, -1e16]2 3// Left to right: 1e16 + 1 rounds straight back to 1e16,4// because 1 is far below the spacing of doubles at that magnitude.5let naive = 06for (const v of values) naive += v7// naive === 0 -- both 1.0 values vanished entirely8 9// Rearranged so the small values meet each other first:10const reordered = (values[1] + values[2]) + (values[0] + values[3])11// reordered === 2 -- the answer you expected12 13// Kahan compensated summation: carry the lost low-order bits.14function kahanSum(xs: number[]): number {15 let sum = 0, c = 016 for (const x of xs) {17 const y = x - c // apply the running compensation18 const t = sum + y19 c = (t - sum) - y // recover what rounding discarded20 sum = t21 }22 return sum23}24// kahanSum(values) === 2Key points
- A fraction is exact in binary only when its denominator is a power of two, so 0.1, 0.2 and 0.3 never were.
- The inputs were rounded before the operation; the arithmetic itself was performed correctly.
- Absolute tolerances fail because representable spacing varies with magnitude — use a relative tolerance with an absolute floor.
- Adding a small value to a much larger accumulator can change nothing at all.
- Floating-point addition is not associative, so parallel reductions can legitimately differ from sequential ones and from each other.
Progressive depth
Overview
0.1 has no exact binary representation, so the value stored was never 0.1. The addition was performed correctly on two approximations and the result rounded again.
Practical
Never compare computed floats for equality; use a relative tolerance with an absolute floor. Never store money in binary floating point. When summing many values, be aware that order matters and that small values can vanish into a large accumulator.
Advanced
Error accumulates roughly with the square root of the operation count for random rounding, but adversarially it can be far worse — catastrophic cancellation, where subtracting two nearly equal values annihilates the significant digits and promotes accumulated noise into the leading position, is the classic failure. Compensated summation recovers most of the lost precision by tracking the discarded low-order bits explicitly.
Internals
Addition aligns exponents by right-shifting the smaller operand's significand, which is where bits are physically lost before the add even happens. Hardware keeps guard, round and sticky bits beyond the stored width to make the final rounding correct, and a fused multiply-add computes a * b + c with a single rounding instead of two — which is why enabling FMA contraction changes results even though it is strictly more accurate. Subnormal values near zero trade precision for reach, and on some hardware are handled at reduced throughput, so a computation drifting into that range can slow down measurably.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Literal → rounding: the decimal in source is rounded to the nearest representable binary value at compile or parse time.
- 2Operands → FPU: the two stored approximations are added, with exponents aligned so the smaller value is shifted right.
- 3Alignment → bit loss: shifting the smaller operand right can push its significant bits past the available width, discarding them.
- 4Exact sum → rounding: the exact result is rounded again to the nearest representable value.
- 5Accumulated error → comparison: repeated rounding leaves a result that differs from the mathematical answer by an amount that grows with the number of operations.
- • Assuming the language or the display is at fault; the stored values genuinely differ, as printing them at full precision shows.
- • Believing a wider type eliminates the problem rather than reducing its rate.
- • Choosing an absolute epsilon without reference to the magnitudes being compared.
- • Treating a differing parallel-reduction result as a race condition when it is order-dependent rounding.
Consequences, controls and cost
- • Equality comparison between computed floating-point values is unreliable and should generally not be used.
- • Summation order changes the result, so parallel and sequential reductions disagree, and runs can differ from one another.
- • Accumulating many small values into a large total silently loses a portion of them.
- • Financial calculations in binary floating point drift from the exact decimal answer in ways audits will notice.
- • Do not use binary floating point for money or any quantity requiring exactness at a fixed decimal scale — use integer minor units or a decimal type.
- • Compare with a relative tolerance plus an absolute floor, and choose the tolerance from the expected error of the computation.
- • Use compensated summation or a wider accumulator when summing many values, especially of differing magnitudes.
- • Sort or group operands so that values of similar magnitude are combined first.
- • Print operands and results at full precision — around 17 significant digits for a double — rather than at default precision.
- • Sum the same data in several orders; the spread between results estimates the accumulated error directly.
- • Recompute a critical result in higher precision and compare, to bound how much error the working precision introduced.
- • Assert that no NaN or infinity has entered a computation at its boundaries, where the cause is still identifiable.
- • Compensated summation roughly doubles the arithmetic in the loop and inhibits some vectorisation, for accuracy most workloads do not need.
- • Decimal types are exact at a fixed scale but substantially slower and generally unsupported by vector units.
- • Sorting before summation costs more than the summation itself unless the data is already ordered.
Scope
§224 — what these claims are specific to.
- GENERALIEEE-754 rounding is specified precisely, so
0.1 + 0.2produces the same bits on every conforming implementation. This is portable behaviour, not a quirk of one platform. - PLATFORM-SPECIFICCompilers may contract a multiply and add into a fused multiply-add with a single rounding, or keep intermediates in wider registers. Both change results slightly and are controlled by optimisation flags, so identical source can differ across builds.