Type 0.1 + 0.2 into almost any language console and you get 0.30000000000000004. It is the most reported non-bug in programming, and the usual explanation — "floating point is imprecise" — is true but useless, because it does not tell you when it will bite you or what to do instead. The imprecision is completely deterministic. Once you can see where it comes from, you can predict exactly which calculations are safe and which are not.
The short version: computers store numbers in binary, and 0.1 has no exact binary representation, in the same way that one third has no exact decimal representation. You can write 0.333… forever and never land on exactly a third. Binary has the same problem with a tenth, and the machine stops writing after a fixed number of digits.
What follows is the mechanism in enough detail to reason about, the specific place it starts costing real money, and the fixes — including the one most people reach for that only hides the problem.
Where the extra digits come from
A double-precision float stores a number as a sign, an exponent, and 52 bits of fraction — the significand. Every value it can hold is some whole number of those 52-bit fractions scaled by a power of two. That set includes 0.5, 0.25, 0.75 and every other sum of halves, quarters and eighths exactly. It does not include 0.1, because a tenth is not a sum of any finite set of binary fractions.
So when you write 0.1, the machine stores the closest value it can, which is very slightly more than a tenth. When you write 0.2, it stores something very slightly more than a fifth. Add the two stored values and you get something very slightly more than three tenths — and the closest storable value to that sum is not the same as the closest storable value to 0.3 written directly. The two differ in the last bit, and that last bit is what surfaces as 0.30000000000000004.
This is why comparing floats with equality is unreliable while comparing them with a tolerance is not. It is also why the error is not random: run it a million times and you get the identical answer every time, on every machine that implements the standard. It is a rounding rule, not noise.
One invoice, followed to the point where it breaks
Take a shopping basket with three items at 0.10, 0.20 and 0.30 in some currency, and a system that stores prices as floats. The subtotal comes out as 0.6000000000000001. Round that for display and it shows 0.60, and everything looks fine. The trouble starts one step later, when something compares the total against a value computed a different way.
Say the payment gateway is sent 0.60 and returns a confirmed amount of 0.60, and your reconciliation code checks whether the stored subtotal equals the confirmed amount. It does not — the stored one carries that trailing 1. On a single order this is a mystery ticket. Across a day of orders it is a reconciliation report that never balances, and the difference is far too small to see in any of the numbers a human looks at.
The damage scales in a specific way: the error is proportional to the size of the number, so a basket of small amounts drifts by an invisible fraction, while an annual total of large amounts drifts by an amount that eventually rounds to a visible unit. Sum enough float currency values and the total is wrong by something a person can notice, with no single line item to blame.
The fix that is not a fix, and the two that are
The common instinct is to round at the end. It genuinely helps for display, and it is not enough on its own, because rounding after the fact cannot recover information the additions already lost, and because it does nothing about equality comparisons made before the rounding. Rounding is a presentation step. Treating it as a correctness step is how the reconciliation bug above survives review.
The first real fix is to stop using fractions at all: store money as an integer number of the smallest unit — cents, satoshi, whatever your currency divides into — and divide only when you display it. Integers are exact, addition of integers is exact, and the failure mode changes from silent drift to overflow, which is loud and easy to test for. This is what most payment systems do internally.
The second is a decimal type: a number stored in base ten rather than base two, so 0.1 is exact by construction. Most ecosystems have one, and the trade is speed and memory — decimal arithmetic runs in software rather than on the floating-point unit. For a ledger that is a fine trade. For a physics loop running millions of times a second it is not, which is the honest reason floats exist and are still the default.
When floats are exactly the right choice
None of this makes floating point a mistake. It was designed for measured quantities, where the input already carries more uncertainty than the representation adds: sensor readings, distances, physical simulation, graphics, statistics, machine-learning weights. If your input is accurate to three digits, an error in the sixteenth is not the thing to worry about, and the speed of hardware floating point is worth a great deal.
The distinction that matters is not "big versus small" or "science versus business". It is whether your values are counted or measured. Counted things — money, votes, inventory, anything where a person can point at a discrepancy of one unit and call it wrong — want integers or decimals. Measured things want floats. Get that division right and most floating-point surprises stop happening.
The one habit worth keeping everywhere: never compare floats with equality. Compare the absolute difference against a tolerance appropriate to your domain. Even in code where floats are the right choice, an equality check is a bug waiting for the day two paths compute the same quantity in a different order — because with floats, addition is not associative, and the order genuinely changes the answer.
Why 'x === expected' is the wrong test for a floating-point result
Given that floating-point arithmetic is deterministic but approximate, comparing a computed floating-point result against an expected value using exact equality is one of the most common practical mistakes this whole subject produces — a calculation that is mathematically correct can still fail an exact-equality check simply because it accumulated a tiny, expected rounding error somewhere along the way. The standard fix is comparing within a small tolerance instead: checking that the absolute difference between the computed and expected value is smaller than some small epsilon, rather than checking for exact identity, which correctly treats a negligible rounding difference as equal while still catching a result that is genuinely, meaningfully wrong.
Choosing an epsilon is not arbitrary, and a bad choice reintroduces the same problem
A tolerance value chosen carelessly — too small, and it fails to absorb genuine, expected rounding error, reintroducing exactly the flaky-test problem this technique exists to solve; too large, and it can silently accept a result that is actually wrong, defeating the purpose of the check entirely — needs to be chosen relative to the actual magnitude and precision requirements of the specific calculation being tested, not copied uniformly from an unrelated test elsewhere in the same codebase, since the right tolerance for comparing two very large numbers is generally different from the right tolerance for comparing two numbers very close to zero.
Why some languages print 0.1 + 0.2 as 0.30000000000000004 and others hide it
The underlying binary value stored for 0.1 + 0.2 is identical across every language built on IEEE 754, but what actually gets printed to the screen differs, because printing involves a separate, independent decision about how many significant digits to display — languages like JavaScript print the shortest decimal string that round-trips back to the exact same binary value, which for this particular sum happens to require showing the visible error, while some other languages or specific formatting functions round to a smaller, fixed number of digits by default, which happens to hide the exact same underlying imprecision from view without actually eliminating it.
The three fixes, and when each one is actually the right one
Rounding for display only fixes what a user sees, without changing the underlying stored value, and is the right choice whenever the exact stored precision does not matter for further calculation, only for presentation. Tolerance-based comparison is the right choice specifically for testing and equality checks, where the goal is confirming a result is close enough to correct rather than storing or displaying it. Switching to integer or arbitrary-precision decimal representation entirely, as covered at greater length elsewhere in this cluster of articles specifically for money, is the right choice whenever exact decimal arithmetic is a genuine requirement rather than a nice-to-have — recognizing which of the three problems is actually present is most of the work; each fix solves a different one of them, and applying the wrong one leaves the actual problem only partially addressed.
A quick mental test for whether a given calculation is actually at risk
Before reaching for any of the three fixes, it is worth asking a simple question: does this calculation ever compare two floating-point values for exact equality, or does it require an exact decimal result rather than an approximately correct one? A calculation that only ever displays a rounded result to a user, or only ever feeds into further approximate calculations like a physics simulation, is very often not actually at risk in any way that matters, and applying the more involved fixes described throughout this article to code that was never actually going to be affected is its own, avoidable form of unnecessary complexity.
Why a linter rule against direct float equality checks is worth enabling
Several static analysis tools can flag a direct equality comparison between two floating-point values automatically, catching the mistake at review time rather than waiting for a flaky test to surface it later — enabling this specific check costs nothing beyond a brief initial pass fixing any existing violations, and it prevents an entire class of future bug from ever being written in the first place rather than relying on every individual developer remembering the rule unaided.
Why this exact example became the canonical teaching case
0.1 and 0.2 are about as simple and ordinary-looking as two numbers could be, which is precisely why their sum failing to equal the expected 0.3 makes such an effective, memorable illustration — a more obscure or complicated example would not carry the same immediate, visceral surprise, and that surprise is exactly what makes the lesson stick well enough that most developers who have heard it once do not forget it, even if the full underlying mechanism, covered in more depth elsewhere in this cluster of articles, takes longer to fully absorb.