Skip to main content

Command Palette

Search for a command to run...

Why 0.1 + 0.2 ≠ 0.3 ....?

Uncovering a Hidden Math Error in My Code

Published
5 min read
Why 0.1 + 0.2 ≠ 0.3 ....?

Computers are incredible at math… except when they aren’t.
If you've ever seen something like this:

0.1 + 0.2  // 0.30000000000000004

you’ve witnessed a floating-point precision error.

Why does this happen?
Why can’t computers represent simple numbers like 0.1 and 0.2 correctly?

In this blog, we’ll break it down simply, then go deep enough to understand the actual internal binary representation behind floating-point numbers.


😨 How did I miss this in my production code?

Initial Implementation

I was building a securities consolidation system that needed to find folios with enough units to absorb ineligible holdings. The logic seemed straightforward:

key := FolioIsin{Folio: folioNumber, Isin: isin}
requiredUnits := usedUnitsForFolioIsin[key] + unitsToAbsorb
if math.Round(Folio.Units*1000)/1000 >= requiredUnits {  // ❌ The bug
    foliosWithExtraUnits = append(foliosWithExtraUnits, Folio)
}

My reasoning: "I'm rounding the external data (Folio.Units) to 3 decimals because that's our precision standard. The calculation on the right is just simple addition. What could go wrong?"

Why I Missed It

1. My mental model was wrong

I thought: "0.1 + 0.2 = 0.3, obviously." But computers think: "0.1 = 0.1000000000000000055..., 0.2 = 0.2000000000000000111..., so their sum is definitely not 0.3."

2. The rounding looked sufficient

I was rounding the external data (folio.Units) and thought that was enough. I didn't realize I was creating an asymmetric comparison - one side normalized, the other side carrying accumulated floating-point errors.

The Fix

The solution was humbling in its simplicity:

key := FolioIsin{Folio: folioNumber, Isin: isin}
requiredUnits := usedUnitsForFolioIsin[key] + unitsToAbsorb
requiredUnits = math.Round(requiredUnits*1000) / 1000 // to avoid floating point precision issues
if math.Round(Folio.Units*1000)/1000 >= requiredUnits {  // ❌ The bug
    foliosWithExtraUnits = append(foliosWithExtraUnits, Folio)
}

Lessons Learned

  1. In financial systems, always normalize floating-point results before comparison - even if the calculation looks "simple"

  2. Asymmetric operations are red flags: If you're rounding/normalizing one side of a comparison, you probably need to do the same to the other side

  3. Floating-point bugs are invisible until they're not: They pass tests, work in staging, then fail mysteriously in production with specific data


🌟 Part 1: Why Floating-Point Errors Happen (Simple Explanation)

Computers don’t store decimals the way humans write them.
They store numbers using binary (base-2).

But many decimal numbers cannot be written exactly as binary fractions, just like 1/3 cannot be written exactly in decimal:

1/3 = 0.333333333… (repeating forever)

Similarly:

0.1 in binary = 0.00011001100110011… (repeating forever)

Since computers have limited bits, they store a rounded version of this infinite binary number → which creates tiny errors.


🌟 Part 2: IEEE-754 — The Actual Format Used by Computers

A floating-point number is stored as:

sign | exponent | mantissa (fraction)

Example for 64-bit double precision:

  • 1 bit → sign

  • 11 bits → exponent

  • 52 bits → mantissa (fraction)

The mantissa is where the approximation happens.


🔎 Part 3: Let’s Actually Convert 0.1 into Binary (Step-By-Step)

This is where things will get interesting.

🎯 Convert the fractional part of 0.1 into binary

We repeatedly multiply by 2 and capture the integer part.

Step 1

0.1 × 2 = 0.2 → integer 0

Step 2

0.2 × 2 = 0.4 → integer 0

Step 3

0.4 × 2 = 0.8 → integer 0

Step 4

0.8 × 2 = 1.6 → integer 1, remainder 0.6

Step 5

0.6 × 2 = 1.2 → integer 1, remainder 0.2

We’re back to 0.2, which already occurred in Step 2, so this pattern repeats forever.


✅ The infinite binary of 0.1 becomes:

0.0001100110011001100110011001100110011… (repeating)

The IEEE-754 format cannot store this infinite repeating sequence.
The mantissa has only 52 bits, so it cuts the number to:

0.1000000000000000055511151231257827021181583404541015625

This is the closest number representable in binary to 0.1.

Yes - the computer never stores exactly 0.1.


🌟 Part 4: Expected vs. Actual Working (with Real Values)

Let’s check three numbers:

DecimalIntended ValueActual Stored Value
0.10.10.10000000000000000555…
0.20.20.20000000000000001110…
0.30.30.29999999999999998889…

Each is slightly off.


💥 Part 5: Why 0.1 + 0.2 becomes 0.30000000000000004

Now let’s add the actual stored values:

0.10000000000000000555
+ 0.20000000000000001110
--------------------------------
  0.30000000000000001665…

Rounded to 16 digits → 0.30000000000000004

So the computer is correct…
It’s just working with slightly imprecise representations.


🌟 Part 6: Why This Happens (Actual Mathematical Reason)

A number can be represented exactly in binary only if it can be written as:

Examples that are exact:

  • 0.5 = 1/2

  • 0.25 = 1/4

  • 0.125 = 1/8

Examples that are not exact (infinite binary):

  • 0.1 = 1/10

  • 0.2 = 1/5

  • 0.3 = 3/10

Why?
Because 10 = 2 × 5 → the factor 5 cannot be represented with powers of 2.

Result → infinite binary → rounding errors.


🌈 Part 7: Visualizing IEEE-754 Representation of 0.1

IEEE-754 stores binary numbers in scientific notation in base-2:

For 0.1, the stored binary pattern is:

0011 1111 1011 1001 1001 1001 1001 1001 1001 1001 1001 1001

Breaking it down:

  • The exponent is adjusted using a bias of 1023

  • The mantissa holds the first 52 bits of the infinite binary sequence

  • Remaining bits are cut off → approximation

This is the root cause of the precision error.


🛠️ Part 8: How to Fix Floating-Point Issues

✔️ 1. Use rounding

round(0.1 + 0.2, 2)  # 0.3

✔️ 2. Use integers (especially for money)

Instead of storing ₹10.25, store 1025 paise.

✔️ 3. Use decimal libraries

Languages offer high-precision decimal types:

  • Python: decimal.Decimal

  • Java: BigDecimal

  • JavaScript: libraries like decimal.js

These store numbers as decimal, not binary.


🏁 Conclusion

Floating-point precision errors aren’t bugs - they’re side effects of how computers store numbers:

  • Numbers like 0.1 and 0.2 turn into infinite binary fractions

  • IEEE-754 cuts them to a limited number of bits

  • Storing approximations leads to tiny errors

  • Those errors show up when doing arithmetic

Understanding this helps you write more reliable numerical code - especially in finance, simulations, and scientific computing.