skip to content

IEEE-754 Floats and Precision

0.1 + 0.2 is the most-asked piece of JavaScript trivia, and the real answer is binary fractions rather than JavaScript being broken. The stronger answer continues into how you would actually compare or store decimal amounts in production.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, why does `0.1 + 0.2 === 0.3` evaluate to false, and what does `0.1 + 0.2` actually produce?

level: juniorimportance: must knowfreq 88%

answer

  1. one numeric type, binary double
  2. one tenth repeats in base two
  3. literals rounded before arithmetic runs
  4. sum lands one step above 0.3
  5. 0.30000000000000004

basics

~10 s

JavaScript numbers are IEEE-754 binary doubles, and 0.1, 0.2 and 0.3 cannot be written exactly in binary. The stored approximations add up to 0.30000000000000004, a different double from 0.3, so === is false.

solid answer

~50 s

Every JavaScript `Number` is an IEEE-754 double-precision binary float: a sign, an 11-bit exponent and 53 significant bits. One tenth in binary is a repeating fraction, the same way one third is repeating in decimal, so the literals `0.1` and `0.2` are each rounded to the nearest representable double before any arithmetic happens. Adding those two approximations gives an exact sum that is itself not representable, so it is rounded again — and it lands one representable step above the double you get from writing `0.3`. The result prints as `0.30000000000000004`, and `0.1 + 0.2 === 0.3` is false. This is not a JavaScript defect; it is how binary floating point behaves in any language that uses it. The practical response is to compare with a tolerance, or to avoid floats entirely for exact domains like money by working in integer minor units.

go deeper

for a junior

Be able to say plainly that numbers are binary doubles, that 0.1 and 0.2 have no exact binary form, and that the sum is 0.30000000000000004. Do not call it a JavaScript bug.

for a middle

Explain the mechanics: 53 significant bits, rounding at parse time and again after the operation, and shortest-round-trip printing that hides the error until it cannot. Name a case that is exact, such as 0.5 + 0.25.

for a senior

Show where this actually bites in production — price times 100 landing below the whole cent, thresholds compared with ===, totals that fail reconciliation — and describe the representation change that removes the class of bug rather than patching one site.

for a principal

Own the policy: decide which domains in the system must be exact and which tolerate bounded error, fix the representation at the boundaries so exactness is not re-litigated per feature, and make rounding a stated, tested rule rather than an incidental toFixed call.

## Every JavaScript number is a double JavaScript has one built-in numeric type for ordinary arithmetic: `Number`, which the specification defines as an IEEE-754 double-precision binary floating-point value. A double is 64 bits: 1 sign bit, 11 exponent bits, and 52 stored fraction bits that give 53 bits of significand (the leading bit is implicit). There is no separate integer type in play here — `1`, `1.5` and `1e300` are all doubles. That format can represent a number only if it can be written as *m × 2^e*, where *m* fits in 53 bits and *e* is in range. Every value you write in source code is rounded to the nearest such number at parse time. ## Why one tenth is not representable In decimal, 1/3 has no finite representation: 0.333… repeats forever. Which fractions terminate depends on the base: a fraction terminates in base *b* only when its denominator's prime factors all divide *b*. Base ten has factors 2 and 5, so 1/2, 1/4, 1/5 and 1/10 all terminate in decimal. Base two has only the factor 2, so only denominators that are powers of two terminate. 1/10 has a factor of 5 in the denominator, so in binary it is the repeating fraction 0.0001100110011… and must be cut off at 53 bits. You can see the stored value by asking for more decimal digits than the shortest printout gives: ```js (0.1).toFixed(20); // "0.10000000000000000555" (0.2).toFixed(20); // "0.20000000000000001110" (0.3).toFixed(20); // "0.29999999999999998890" ``` None of the three is the decimal number you typed. `0.1` is stored slightly high, `0.3` slightly low. ## Three roundings, not one The error people blame on `+` actually accumulates in three steps: 1. `0.1` rounds to the nearest double (slightly above one tenth). 2. `0.2` rounds to the nearest double (slightly above two tenths). 3. The exact mathematical sum of those two doubles is computed, then rounded to the nearest double. ```js (0.1 + 0.2).toFixed(20); // "0.30000000000000004441" ``` That result is one representable step above the double stored for the literal `0.3`, so the two are genuinely different values and `===` correctly reports false. Strict equality is not being clever or lenient here; it is comparing two distinct doubles. The same effect shows up wherever binary rounding is involved, not just in addition: ```js 0.1 * 3; // 0.30000000000000004 0.07 * 100; // 7.000000000000001 4.35 * 100; // 434.99999999999994 ``` The last one is the classic money bug: a price of 4.35 multiplied by 100 to get "cents" is below 435, so truncating gives 434. ## Why printing usually looks clean If `0.1` is really 0.1000000000000000055…, why does `console.log(0.1)` print `0.1`? Because JavaScript's number-to-string conversion prints the *shortest decimal string that round-trips* — the fewest digits that, when parsed back, yield the same double. For the double nearest one tenth, that string is `"0.1"`. For the sum above, no short string round-trips, so you get the full `0.30000000000000004`. The ugly digits are not noise added by printing; they are the moment the approximation stops being hideable. ## What is exact Plenty of arithmetic is exact. Fractions whose denominators are powers of two are represented perfectly, so `0.5 + 0.25 === 0.75` is true, and `0.125` is stored exactly. Whole numbers are also exact as long as they stay within the range where 53 bits can hold every integer, which is why counting, indexing and array lengths never surprise anyone. The result is also fully deterministic. IEEE-754 specifies round-to-nearest-even for these operations and the language specification requires it, so `0.1 + 0.2` produces the identical double in every conforming engine, on every platform. This is a portable, predictable approximation, not a random one. ## What to do instead For comparisons of computed values, test that the difference is small relative to the magnitudes involved rather than testing for exact equality. For exact decimal domains — currency above all — do not store the amount as a fractional double at all: keep it in integer minor units (cents) or as a decimal string, do the arithmetic in integers, and convert to a decimal presentation only at the edges. And do not reach for `toFixed` as a repair: it formats a double that is already the wrong value, so it cannot recover precision that was lost when the literal was parsed.

  • Which decimal literals are stored exactly as doubles, and can you name one?
    Any fraction whose denominator is a power of two, within the exponent range — 0.5, 0.25, 0.125, 0.75. So `0.5 + 0.25 === 0.75` is true and `(0.125).toFixed(20)` shows a clean value. Denominators with a factor of 5, like tenths and hundredths, always repeat in binary and must be rounded.
  • Does `0.1 + 0.2` give the same result in every JavaScript engine and on every platform?
    Yes. The language specification pins `Number` to IEEE-754 double precision with round-to-nearest-even, so the operation is fully determined. Every conforming engine on every CPU produces the identical double, `0.30000000000000004`. The imprecision is portable and reproducible, which is exactly why you can write reliable tolerance checks against it.
  • If `0.1` is really slightly larger than one tenth, why does printing it show `0.1`?
    Number-to-string conversion emits the shortest decimal string that parses back to the same double. For the double nearest one tenth that string is `"0.1"`. Ask for more digits with `(0.1).toFixed(20)` and the stored value `0.10000000000000000555` appears. Printing hides the approximation; it does not remove it.

saying these in an interview costs you the question

  • Says JavaScript's addition operator is buggy
  • Claims JavaScript uses 32-bit floats so precision is low
  • Thinks the literals are exact and only + rounds
  • Believes toFixed(2) makes the arithmetic exact again
  • Says the result differs between engines or machines

context

open as a page

How should you compare two computed JavaScript numbers for approximate equality, and what does `Number.EPSILON` actually represent?

level: middleimportance: must knowfreq 58%

basics

~20 s

Number.EPSILON is the gap between 1 and the next representable double, about 2.22e-16 — a relative spacing near 1, not a universal error bound. Compare with a tolerance scaled to the operands' magnitude, not a fixed epsilon.

open as a page

In JavaScript, `(1.005).toFixed(2)` returns "1.00" rather than "1.01". Why, and what else should you know about `Number.prototype.toFixed`?

level: middleimportance: should knowfreq 46%

basics

~20 s

The double stored for the literal 1.005 is 1.00499999999999989, just under the halfway point, so toFixed rounds it down. toFixed rounds the stored binary value, not the decimal you typed, and it returns a string.

open as a page

How would you represent and compute monetary amounts in a JavaScript service so that totals do not drift, and where should the conversions between representations happen?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Never hold money in a fractional double. Keep amounts as integers in the smallest unit — cents — or as decimal strings, do all arithmetic on the integers, apply one explicit rounding rule where division forces one, and convert to decimal only at the system's edges.

open as a page

A JavaScript reporting job sums millions of floating-point amounts, and its totals disagree with the source ledger by small amounts that change when the rows are processed in a different order. How do you diagnose this and decide on a fix?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Floating-point addition is not associative: each partial sum is rounded, so a different order gives a different total, and large running sums absorb small addends entirely. Diagnose by comparing against an exact integer sum, then decide whether the domain needs exactness or bounded error.

open as a page