Advertisement

Type 0.1 + 0.2 into almost any programming language and you get 0.30000000000000004. Every developer meets this eventually, usually with alarm, often concluding the language is broken. It is not a bug — it is the predictable result of how computers store fractional numbers, and understanding it prevents real defects in anything that touches money, measurements or comparisons.

The short version: computers store numbers in binary, and just as one-third cannot be written exactly in decimal (0.333...), many everyday decimals cannot be written exactly in binary.

Binary can't hold every decimal

In base ten, we accept that 1/3 has no exact decimal form. In base two, the same problem hits numbers that look perfectly clean to us: 0.1 and 0.2 have no exact binary representation. The computer stores the closest value it can fit in the bits available, which is very slightly off. Add two slightly-off values and the small errors combine into the visible 0.30000000000000004.

This is not sloppiness; it is a fundamental trade-off. Floating-point numbers pack an enormous range into a fixed number of bits by storing an approximation, which is exactly right for scientific and graphical work and exactly wrong for anything that must be penny-perfect.

Advertisement

The two rules that matter

Rule one: never compare floating-point numbers for exact equality. Checking whether 0.1 + 0.2 equals 0.3 will fail. Instead, check whether the difference between them is smaller than a tiny tolerance (an "epsilon"). Code that says "if the result equals exactly X" over floating-point math is a bug waiting for the wrong input.

Rule two: never store money as a floating-point number. Rounding errors that are invisible in a physics simulation become missing cents in an invoice and failed reconciliations in accounting. The fix is to work in the smallest whole unit — store cents as integers — or use a decimal type designed for exact base-ten arithmetic.

Choosing the right tool

Floating-point is the correct choice for the vast domain it was built for: measurements, graphics, machine learning, simulations, anything where a vanishingly small relative error is irrelevant. For those, its speed and range are exactly what you want, and the tiny imprecision never matters.

For exact decimal needs — currency, precise counters, anything a human will audit to the last digit — reach for integers-of-the-smallest-unit or a dedicated decimal/big-decimal type. The mark of an experienced developer is not memorising the binary representation of 0.1, but knowing which number type each job demands and never comparing floats with equals.

Advertisement

Why binary cannot represent most decimal fractions exactly, mechanically

A decimal fraction like 0.1 is exact in base 10 because ten is the base being used, but expressed in binary it becomes a repeating fraction, exactly the way 1/3 is a repeating decimal in base 10 despite being a perfectly simple fraction in base 3 — and a computer's floating-point format has only a fixed, finite number of bits to store that repeating binary fraction in, which means it necessarily gets truncated at some point, storing the closest representable value rather than the true one. This is not a flaw in any particular language's implementation; it is a direct, unavoidable mathematical consequence of representing certain base-10 fractions in base 2 with a finite number of bits, and every language built on the IEEE 754 standard, which is nearly all mainstream languages, inherits exactly the same behavior.

Mantissa and exponent: what the 64 bits of a double actually hold

A standard double-precision float allocates its 64 bits into three parts: one sign bit, an 11-bit exponent, and a 52-bit mantissa (also called the significand), which together encode a value conceptually similar to scientific notation — a significant-digits part and a scale part — but in binary rather than decimal. The mantissa's 52 bits give roughly 15 to 17 significant decimal digits of precision, which is why floating-point numbers are quite accurate for most everyday purposes and only visibly misbehave in the specific circumstances this cluster of articles describes: when an exact decimal value is needed, or when many small rounding errors accumulate across a long chain of operations.

Advertisement

Why the imprecision is deterministic, not random

It is a common and understandable misconception that floating-point error is a kind of random noise; it is in fact fully deterministic — the exact same operation on the exact same inputs produces the exact same, precisely reproducible result every single time, on any conforming IEEE 754 implementation, because the standard specifies exactly how rounding happens at every step. This determinism is actually why the specific 0.1 + 0.2 example is so famous and so consistently reproducible across different languages and different machines: it is not that the error varies unpredictably, it is that the exact same, precisely predictable approximation happens every time, which is also why the same worked-around behaviors (rounding for display, comparing with tolerance) reliably fix it every time as well.

Special values: infinity, negative zero, and NaN

Beyond ordinary approximated numbers, the IEEE 754 standard reserves specific bit patterns for special values that behave in ways worth knowing explicitly: positive and negative infinity, produced by an operation like dividing a positive number by zero rather than throwing an error the way integer division by zero typically does; negative zero, a value that compares equal to positive zero but can produce a different result in specific operations like division; and NaN (not a number), produced by an operation with no sensible numeric result, like zero divided by zero, which has the famously surprising property of comparing unequal to every value, including itself, meaning `NaN === NaN` evaluates to false in essentially every language that follows the standard.

Why single and double precision floats round differently in practice

A 32-bit single-precision float allocates fewer bits to its mantissa than a 64-bit double does, which means it can represent noticeably fewer significant digits accurately — a calculation performed in single precision can show visible rounding error at a much smaller number of operations than the same calculation would in double precision, which is exactly why most general-purpose application code defaults to double precision, reserving single precision for situations, like large-scale graphics or machine learning computation, where the reduced memory footprint and faster computation are worth the accuracy trade-off.

Why floating-point comparison chains can behave non-intuitively

Because rounding happens independently at each individual operation, a chain of floating-point operations that would be mathematically equivalent if computed in a different order can produce subtly different results depending on the actual order operations are performed in — `(a + b) + c` and `a + (b + c)` are not guaranteed to produce the identical floating-point result even though they are mathematically identical, which is a genuinely surprising property to anyone assuming floating-point arithmetic obeys the same associativity rules as real-number arithmetic, and it is exactly why some numerical algorithms are deliberately written to sum values in a specific, carefully chosen order to minimize this compounding error.

Why some numbers are represented exactly and others are not

Not every fractional value suffers from this imprecision — a value like 0.5 or 0.25 is represented exactly in binary floating point, because those specific fractions happen to be exact sums of negative powers of two (0.5 is 2⁻¹, 0.25 is 2⁻²), while 0.1 is not, since it has no finite representation as a sum of powers of two at all; recognizing that the imprecision is specific to certain values rather than universal to all fractions clarifies why some floating-point calculations produce perfectly exact results while others, involving values like 0.1 or 0.3, reliably do not.

Why arbitrary-precision arithmetic exists as an alternative, and what it costs

Some languages and libraries offer arbitrary-precision decimal or rational arithmetic, representing numbers exactly regardless of how many digits they require rather than approximating them within a fixed number of bits, which eliminates the entire class of imprecision this article describes — at the cost of being considerably slower than native floating-point hardware operations, since there is no dedicated processor instruction for arbitrary-precision math the way there is for standard floating-point, which is why it is reached for selectively, in the specific calculations that need exactness, rather than universally in place of ordinary floats.

Why the same 0.1 + 0.2 example works identically in nearly every mainstream language

Trying the same 0.1 + 0.2 calculation in Python, Java, C, Ruby, or Go produces the identical underlying binary result in every one of them, because all of these languages implement the same IEEE 754 double-precision standard for their default floating-point type — this cross-language consistency is itself a useful diagnostic: a floating-point quirk this widely reproducible across otherwise very different languages is a strong signal that the cause lives in the shared numeric representation itself, not in any particular language's own implementation choices.

Why understanding this mechanism prevents a specific class of premature optimization

A developer who does not understand why floating-point comparisons need tolerance sometimes overcorrects by avoiding floating-point types altogether even in contexts, like graphics or physics simulation, where the approximation is entirely acceptable and switching to a slower exact-arithmetic type would be a genuine, unnecessary performance cost — understanding precisely which situations are actually at risk, as this cluster of articles lays out, is what allows using ordinary floating-point confidently in the many contexts where its approximation is fine, reserving the more careful handling for the specific contexts where it is not.

Why this is taught early but rarely explained well

Most programming courses mention that '0.1 + 0.2 does not equal 0.3' as an interesting trivia fact without explaining the actual binary representation mechanism behind it, which leaves many developers knowing the symptom by rote without understanding the cause well enough to reason about when it will or will not bite them in a genuinely new situation — understanding the mantissa-and-exponent mechanism described earlier in this article converts that memorized trivia into a piece of knowledge that actually generalizes to new, unfamiliar cases.

Why this knowledge transfers directly to understanding floating-point bugs in other domains

The same underlying mechanism explains a range of seemingly unrelated floating-point oddities beyond simple arithmetic — why a loop counter incremented by a fractional amount can run one iteration more or fewer than expected, why a graphics shader's colors can shift subtly at certain coordinates — and recognizing all of these as the same root cause, rather than as separate unrelated mysteries, is exactly what understanding the mechanism, rather than memorizing the single famous example, actually buys.

Why a quick binary-to-decimal conversion by hand builds lasting intuition

Manually converting a handful of simple binary fractions to decimal — 0.1 in binary, 0.01 in binary — and seeing directly which ones terminate cleanly and which repeat indefinitely is a small, concrete exercise that builds durable intuition for why certain decimal values are exact in floating point and others are not, in a way that reading the explanation alone often does not fully cement.