Why 0.1 + 0.2 Doesn't Equal 0.3: Floating-Point Explained
Floating-point numbers can't represent most decimals exactly in binary — that's why 0.1 + 0.2 gives 0.30000000000000004, and what IEEE 754 actually stores.
Floating-point numbers are the standard way computers approximate real numbers using a fixed number of bits, and the approximation is the whole story: most decimal fractions can’t be represented exactly in binary, so arithmetic on them accumulates small rounding errors. That’s why 0.1 + 0.2 prints 0.30000000000000004 in JavaScript, Python, Java, and nearly every other language — they all use the same underlying format, and the error isn’t a bug in any of them.
Why decimals don’t fit cleanly in binary
The problem isn’t specific to computers; it’s specific to base 2. In decimal, 1/3 can’t be written exactly with a finite number of digits — you get 0.3333… forever. Binary has the same issue with different numbers. 1/10 (0.1 in decimal) is one of them: in binary it’s a repeating fraction, 0.0001100110011…, that never terminates. A computer has to cut it off at some fixed number of bits, and that truncation is where the error enters.
0.1 and 0.2 each get rounded to the nearest representable binary fraction. Neither rounding error is large enough to notice on its own, but when you add the two approximations together, the sum doesn’t land exactly on the binary value for 0.3 — it lands one bit off, and printing the result reveals the discrepancy.
The IEEE 754 format
Nearly every mainstream language uses the IEEE 754 standard for floating-point arithmetic, typically in “double precision” (64 bits). A double splits its bits into three fields:
- Sign (1 bit) — positive or negative.
- Exponent (11 bits) — scales the number up or down, like scientific notation’s power of ten.
- Mantissa / significand (52 bits) — the significant digits of the number.
This is essentially scientific notation in binary: a number is stored as ± mantissa × 2^exponent. The exponent lets the same 64 bits represent both tiny numbers (like 0.0000001) and huge ones (like 100000000000), which is why floats have enormous range but only about 15-17 decimal digits of precision — the mantissa has a fixed number of bits no matter how the exponent scales things.
Single precision (32 bits — 1 sign, 8 exponent, 23 mantissa) is the same idea with less precision, common in graphics and machine learning where speed and memory matter more than exactness. AI accelerators like TPUs often go further, using narrower formats such as bfloat16, trading precision for throughput — a deliberate version of the same rounding behavior described here.
Where the rounding actually shows up
Every floating-point operation — addition, subtraction, multiplication, division — can introduce rounding, because the exact mathematical result often needs more mantissa bits than the format has. The computer rounds to the nearest representable value and moves on. Individually these errors are tiny (on the order of 10⁻¹⁶ for doubles), but two properties make them dangerous in aggregate:
- They compound. Summing many floats in a loop accumulates rounding error with each addition. Summing a million small numbers can produce a noticeably wrong total.
- Order matters.
(a + b) + canda + (b + c)can give different results in floating-point, because each intermediate sum rounds differently. This is why floating-point addition isn’t strictly associative, which surprises anyone coming from pure math.
Practical consequences
The most common place this bites developers is money. Representing currency as floats means $19.99 + $0.01 might not equal exactly $20.00 after enough operations, and rounding errors can silently drift a running balance over time. The standard fix is to store money as integer cents (or the smallest currency unit) and only convert to a decimal display value at the edges — never do arithmetic on the decimal form.
Equality comparisons are the other classic trap: never write if (x == 0.3) when x was computed through floating-point arithmetic. Compare against a small tolerance (Math.abs(x - 0.3) < 1e-9) instead, since the computed value is almost certainly not bit-for-bit equal to the literal.
For values that must be exact whole numbers beyond what a standard float can represent precisely, JavaScript’s BigInt is a separate integer type with no rounding, and most databases have dedicated fixed-point DECIMAL/NUMERIC types for exactly this reason — they store digits, not binary fractions, so they represent decimal values like 19.99 exactly.
A concrete example
Take a running total computed by adding 0.1 one million times. Mathematically the answer is 100000.0 exactly. In floating-point, each addition rounds to the nearest representable double, and those roundings don’t cancel out — they drift, typically landing noticeably off from the exact value by the time the loop finishes. The error in any single addition is minuscule, on the order of one part in 10^16, but a million additions is a million chances for that error to accumulate in the same direction. This is why numerical code that sums long sequences often uses compensated summation techniques (like Kahan summation) specifically to keep that drift in check, rather than trusting naive addition to stay accurate over many iterations.
Fixed-point vs floating-point
| Fixed-point / decimal | Floating-point | |
|---|---|---|
| Precision | Exact for the digits it stores | Approximate for most decimal fractions |
| Range | Limited, fixed scale | Huge — scales via the exponent |
| Speed | Often slower (software or scaled-integer math) | Fast — native hardware support |
| Best for | Currency, exact decimal values | Scientific computing, graphics, ML |
The byte order used to lay these bits out in memory follows the same conventions as integers — see big-endian vs little-endian for how that works — but the IEEE 754 encoding itself is identical regardless of endianness.
The takeaway
Floating-point numbers trade exactness for range and speed: they store a sign, an exponent, and a fixed-width mantissa, and most decimal fractions simply don’t have an exact binary representation. The rounding this produces is small per operation but compounds across a computation, so treat floating-point equality checks with suspicion, keep money in integer or fixed-point form, and reach for arbitrary-precision types when a value genuinely needs to be exact.
Keep reading
Chisato · · 4 min read Thermal Interface Materials Explained
Thermal interface material fills microscopic gaps between a chip and its heatsink so heat can actually transfer to the cooler.
Chisato · · 4 min read Clock Speed vs. IPC: What Actually Makes a CPU Fast
Clock speed measures cycles per second; IPC measures work done per cycle. Real CPU performance is the product of both, not either one alone.
Chisato · · 4 min read UMA vs NUMA: Memory Architecture Explained
UMA gives every CPU core equal-latency memory access; NUMA gives each core faster access to its local memory bank. How the two architectures differ.