Skip to main content

Why 0.1 + 0.2 ≠ 0.3: Reading the IEEE 754 Bits Behind the Most Famous Bug in Programming

Decode the actual sign, exponent, and mantissa bits that make 0.1 + 0.2 print 0.30000000000000004 — with real hex words you can verify yourself.

Published
#ieee-754 #floating-point #binary #debugging #dev

Why 0.1 + 0.2 ≠ 0.3: Reading the IEEE 754 Bits Behind the Most Famous Bug in Programming

Open any JavaScript console and type 0.1 + 0.2. You get 0.30000000000000004. Every language that uses hardware floats — Python, Java, C, Rust, Go — produces the same answer, because they all store numbers the same way: the IEEE 754 binary format your CPU implements in silicon.

Most explanations stop at "floats are imprecise." That is true but useless when you are staring at a failing test assertion at 11pm. The useful version is being able to read the actual bits, because the bits tell you exactly how far off two values are and whether the difference matters. This article walks through the three bit patterns involved in 0.1 + 0.2 === 0.3, and you can paste every value into the IEEE 754 Floating Point Converter to check my work as you read.

The three fields inside every float

A 64-bit double splits into three fields: 1 sign bit, 11 exponent bits, and 52 mantissa bits. The value is reconstructed as (−1)^sign × 1.mantissa × 2^(exponent − 1023). That bias of 1023 trips up almost everyone the first time: the exponent field stores 01111111011 (1019) for 0.1, but the real exponent is 1019 − 1023 = −4, because 0.1 lies between 2⁻⁴ (0.0625) and 2⁻³ (0.125).

The precision limit is the part that matters for our bug. With 52 explicit mantissa bits plus the implied leading 1, a double carries 53 significant binary digits, which works out to log₁₀(2⁵³) ≈ 15.95 decimal digits — per the IEEE 754-2019 standard's binary64 definition, that is why every language documents doubles as "15 to 17 significant digits." Anything your decimal number needs beyond bit 53 gets rounded away, and that rounding is where 0.00000000000000004 comes from.

What 0.1 actually looks like in memory

0.1 has no finite binary representation. In base 2 it is 0.000110011001100110011… with 0011 repeating forever, the same way 1/3 repeats in base 10. The CPU has to cut that infinite tail at 52 mantissa bits and round.

Here is the real input/output. Paste 0.1 into the converter in double precision and you get:

input:    0.1
hex:      0x3FB999999999999A
sign:     0
exponent: 01111111011  (raw 1019, real −4)
mantissa: 1001100110011001100110011001100110011001100110011010
decoded:  0.1000000000000000055511151231257827021181583404541015625

Look at the last mantissa digits: …1010, not …1001. The infinite 1001 pattern was rounded up at bit 52, so the stored value is a hair above 0.1 — about 5.55 × 10⁻¹⁸ too big. Do the same for 0.2 and you get 0x3FC999999999999A: identical mantissa, exponent one higher, also rounded up.

The one-bit difference that breaks the equality

Now the punchline. Add the two stored values and the sum has to be rounded again to fit in 52 bits. Compare the result against the literal 0.3:

0.1 + 0.2  →  0x3FD3333333333334
0.3        →  0x3FD3333333333333

The two hex words differ in exactly the last digit — one unit in the last place (1 ULP), the smallest possible gap between two adjacent doubles at this magnitude, about 5.55 × 10⁻¹⁷. Both rounding errors in 0.1 and 0.2 happened to go upward, the addition carried them past the midpoint, and the sum landed one representable number above where the literal 0.3 rounds to. That is the entire bug: not randomness, not a language flaw, just two upward roundings stacking.

This is also why the standard fix — Math.abs(a - b) < Number.EPSILON style comparison — is correct rather than a hack. You are explicitly allowing for the ULP-scale noise that the format guarantees will exist.

I tested the byte-order trap so you don't have to

When I was verifying the hex words for this article, I deliberately fed the converter the four bytes of a single-precision float the way they actually arrive off the wire in a little-endian memory dump: 00 00 28 42. Decoded as-is, that pattern is meaningless garbage (a subnormal around 1.4 × 10⁻⁴¹ — the converter's class badge flags it as subnormal, which was my first hint something was wrong). Reversed to 0x42280000, it decodes cleanly to 42.0: sign 0, raw exponent 132, real exponent 5, value 1.3125 × 2⁵.

That subnormal badge is genuinely the fastest endianness detector I know. If a "float" from a binary protocol decodes to something absurdly tiny with an all-zero exponent field, you almost certainly read the bytes backwards. Flip them and decode again before you blame the sender. I also cross-checked the raw exponent arithmetic with the Base Converter0x42280000 in binary starts 0100 0010 0010 1000…, and bits 2–9 are 10000100, which is 132 in decimal, confirming the real exponent of 132 − 127 = 5.

When the bits are the only honest answer

A few situations where reading raw float bits beats every other debugging approach:

Serialization round-trips. If your hand-rolled encoder for an embedded target emits 0x3E200000 for 0.15625 and the converter agrees, your sign/exponent/mantissa packing is provably correct. 0.15625 is a great test value precisely because it is exact in binary (1.25 × 2⁻³) — any discrepancy is a packing bug, never rounding.

NaN forensics. A computation returning NaN tells you nothing about which NaN. The bit pattern does: 0x7FC00000 is the standard quiet NaN, but some platforms encode diagnostic payloads in the mantissa bits. Pasting the hex into the converter shows the payload instantly.

Masking and bit surgery. Sometimes you need to manipulate the fields directly — clearing the sign bit to get a branch-free abs(), or extracting the exponent with a shift and mask. Work out the mask values in the Bitwise Calculator first: 0x7FFFFFFF AND any single-precision word strips the sign, and you can verify the result decodes to the same magnitude.

Float versus double truncation. Casting a double to a 32-bit float throws away 29 mantissa bits. Paste the same decimal in both precisions and diff the decoded values — for 0.1, single precision stores 0.100000001490116…, an error 10⁸ times larger than the double's. If a sensor pipeline mixes the two widths, this comparison shows you precisely how much accuracy each cast costs.

The takeaway

0.1 + 0.2 !== 0.3 stops being mysterious the moment you see …334 next to …333. The format is doing exactly what 52 mantissa bits can do, and the error is bounded, predictable, and visible. The next time a float comparison fails or a binary protocol hands you four suspicious bytes, skip the guessing: drop the value into the IEEE 754 converter, read the three fields, and the bug usually identifies itself.


Made by Toolora · Updated 2026-06-13