Skip to content
Spellkit

Why Money Math Breaks in Floating Point

0.1 + 0.2 isn't 0.3 because binary can't write one tenth. The same limitation puts a stray cent in payroll totals and amortization schedules.

Type 0.1 + 0.2 into any JavaScript console and you get 0.30000000000000004. This is the most reported non-bug in programming, and the explanation — floating point is imprecise — is true but unhelpfully vague. The precise version is more interesting, and it tells you exactly when the problem will bite and what to do instead.

Binary can't write one tenth

The issue isn't that floating point is fuzzy. It's that it's binary.

In decimal, ⅓ has no finite representation: 0.333… forever. That's not a flaw in decimal, it's a consequence of 3 not dividing any power of 10. Write ⅓ with finite decimal digits and you must round.

Binary has the same problem with different fractions. A binary fraction can exactly represent a value only when its denominator is a power of two — ½, ¼, ⅛, and sums of those. One tenth is not. In binary, 0.1 is 0.0001100110011… repeating forever, exactly as ⅓ repeats in decimal.

IEEE 754 double precision gives 53 bits of significand, so the stored value is the nearest representable double to 0.1 — approximately 0.1000000000000000055511151231257827. Same story for 0.2. Add those two slightly-wrong values and the exact sum isn't the nearest double to 0.3, so it prints as 0.30000000000000004.

Every step here is deterministic and correct. Nothing is random or fuzzy. The arithmetic is exact; the inputs couldn't be written down exactly in the first place.

Doubles are extremely precise — about 15–17 significant decimal digits. The trouble is that money doesn't need enormous precision; it needs exactness at two decimal places, and exactness is the one thing binary fractions can't offer for tenths and hundredths.

How a stray cent gets into a real system

Precision this fine seems harmless until small errors accumulate or get compared.

Accumulation. Sum 0.1 ten thousand times in doubles and the result is not 1000. Each addition rounds, and the errors, while not always in the same direction, don't fully cancel. A payroll run that totals thousands of line items in floats will disagree with the sum of the printed values by a few cents — and the printed values are what someone will check against.

Comparison. if (balance === 0) fails when balance holds a residue of 1e-17 instead of zero. Loops that decrement a balance until it reaches exactly zero can overshoot and run forever, or stop one iteration early.

Rounding at boundaries. The value stored for 2.675 is fractionally below 2.675, so rounding it to two places gives 2.67, not the 2.68 everyone expects. The rounding function is behaving correctly on the number it was actually given.

Order dependence. Floating point addition isn't associative: (a + b) + c can differ from a + (b + c). Two reports that sum the same transactions in different orders can produce different totals, which is exactly the kind of discrepancy that costs a day to investigate.

What to use instead

Integer minor units. Store amounts as whole cents — ₩ and ¥ in whole units, USD and EUR in cents. 1050 rather than 10.50. Integers are exact, addition and subtraction are exact, and comparison is exact. Convert to a display string only at the edges. This is what most payment systems do, and it's why APIs like Stripe's take amounts in cents.

The complication is division: splitting 1000 cents three ways gives 333, 333, 333 and loses a cent. That cent doesn't vanish because floats would have handled it — it's a genuine allocation question, and you have to decide deliberately who gets it. Making the loss visible is the point.

Decimal types. Where the domain genuinely needs fractional units — interest rates, tax rates, per-unit pricing to four places — use a decimal type that represents values in base 10: DECIMAL(19,4) in SQL, BigDecimal in Java, decimal.Decimal in Python, decimal in C#. These store 0.1 exactly and produce the answers a person doing the arithmetic on paper would get. They're slower than hardware floats, which matters approximately never for financial code.

JavaScript's answer is BigInt for minor units, or a decimal library. Number is a double and always will be.

Choose a rounding mode explicitly. "Round half up" is what most people expect. "Round half to even" — banker's rounding — sends exact halves to the nearest even digit, which removes the small upward bias that always-round-up introduces across many operations, and is required by some accounting standards. The important part is that it's a decision recorded somewhere, not a default nobody examined.

Where it shows up in ordinary calculators

Amortization. The standard payment formula produces a value with many decimal places, which then gets rounded to the nearest cent. Multiply that rounded payment by the number of periods and it won't equal principal plus total interest. Real lenders handle this by computing each period's interest on the exact outstanding balance and adjusting the final payment to absorb the accumulated difference — which is why the last row of a proper schedule is often a few cents off from every other row. A loan calculator whose final balance lands exactly on zero is doing this; one that leaves a residue is showing you unreconciled floating point. The mechanics of the schedule itself are covered in how loan amortization works.

Percentages. Computing 10% of 1050 cents gives 105 exactly. Computing 10% of 10.50 in doubles gives 1.0500000000000000444. Both round to the same displayed value here, but chain several percentage operations — a discount, then tax, then a split — and the drift becomes visible. Working in minor units keeps every intermediate exact, which is worth doing even in a simple percentage calculator.

Payroll. Deductions computed as percentages of gross, each rounded to the won or cent, generally won't sum to the difference between gross and net. Every salary calculation needs a defined rule for where the remainder lands.

The one-line version

Floating point is the right tool for physical quantities, where inputs are measurements with their own uncertainty and 1e-16 of relative error is meaningless. Money is not a measurement. It's a count of indivisible units with legally defined rounding, and it belongs in a representation that can count them exactly.