JAVASCRIPT / NUMBERS, MATH, AND BIGINT
Integers, floats, and floating-point error
Explain why JavaScript's single number type makes 0.1 + 0.2 !== 0.3, predict which decimals are exact, and compare or store fractional values safely.
What you will learn
- Predict which decimal literals a double can hold exactly: only k / 2^n values
- Compare fractional results with a magnitude-scaled tolerance instead of ===
- Store money and other counted values as integers in the smallest unit
- Inspect the value actually stored with toFixed(20) instead of trusting the print
Understanding Integers, floats, and floating-point error
JavaScript has exactly one numeric type for ordinary numbers, and it is a float: every value of type number is an IEEE 754 binary64 double, made of a sign, an 11-bit exponent, and 53 bits of significand. That is why typeof 3 and typeof 3.5 both report number, and why 3 === 3.0 is true, since there is no separate integer type to convert between. Number.isInteger(3.0) returns true because it asks a question about the value (does it have a fractional part?), not about a type. Whole numbers up to 2^53 fit in the significand exactly, so integer arithmetic in that range is precise; the trouble starts with fractions.
A double stores a number as significand x 2^exponent, so the only fractions it can represent exactly are those whose denominator is a power of two: 0.5, 0.25, 0.75, 1.5, 0.0625. Written in binary, 0.1 is 0.0001100110011... repeating forever, exactly the way 1/3 is 0.333... in decimal, and 53 bits of significand force that expansion to be cut off and rounded. So the literal 0.1 already means 0.10000000000000000555..., 0.2 means something slightly over 0.2, their exact sum gets rounded once more to fit, and the double you land on is one step above the double you get from writing 0.3, which is why the equality test fails.
The useful mental model is that this is deterministic representation error, not noise: each operation returns the exactly rounded result of the true operation on the values actually stored, so the same expression always produces the same wrong-looking digits. Two consequences follow. Addition is commutative but not associative, so (0.1 + 0.2) + 0.3 and 0.1 + (0.2 + 0.3) give different answers, and errors can also cancel, which is why some sums come out looking perfect by luck. The practical split is floats for measured quantities where a tiny relative error is acceptable, integers in the smallest unit for anything counted, and tolerance-based comparisons whenever you must test fractional values for equality.
placeholder
// One number type: 3 and 3.0 are the same 64-bit binary float.
console.log(typeof 3, typeof 3.5, 3 === 3.0);
// Fractions with a power-of-two denominator are stored exactly.
console.log(0.5 + 0.25 === 0.75);
// 0.1 repeats forever in binary, so it is rounded to 53 bits.
console.log((0.1).toFixed(20));
// Two already-rounded inputs, rounded again after the addition.
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);Every JavaScript number is a 64-bit binary float, so any decimal that is not a sum of powers of two is stored as a nearby approximation and every operation carries that approximation forward.
Worked examples
Comparing with a tolerance that fits the scale
Shows why a fixed epsilon is not a general equality test and how a relative tolerance fixes it.
const eqEpsilon = (a, b) => Math.abs(a - b) < Number.EPSILON;
const eqRelative = (a, b, rel = 1e-9) =>
Math.abs(a - b) <= rel * Math.max(Math.abs(a), Math.abs(b));
console.log(0.1 + 0.2 === 0.3);
console.log(eqEpsilon(0.1 + 0.2, 0.3));
console.log(eqEpsilon(1e-20, 2e-20));
console.log(eqRelative(1e-20, 2e-20));
console.log(eqRelative(0.1 + 0.2, 0.3));Example explained
Line 1The gap between 0.1 + 0.2 and 0.3 is exactly 2^-54, about 5.6e-17, a single rounding step.
Line 2Number.EPSILON is 2^-52, the distance from 1 to the next double, so it sizes that step correctly only for values near 1.
Line 3eqEpsilon(1e-20, 2e-20) is true even though one value is twice the other, because their whole difference is smaller than the fixed tolerance.
Line 4eqRelative multiplies the tolerance by the operands' magnitude, so it rejects 1e-20 vs 2e-20 while still accepting the 0.1 + 0.2 result.
Accumulating decimals versus counting in cents
Demonstrates how repeated rounding drifts a running total and how integer units remove the drift.
let floatTotal = 0;
for (let i = 0; i < 10; i++) floatTotal += 0.1;
console.log(floatTotal, floatTotal === 1);
let cents = 0;
for (let i = 0; i < 10; i++) cents += 10;
console.log(cents, cents / 100 === 1);Example explained
Line 1Every += rounds the running total to the nearest double, and those roundings do not cancel: the result is 1 - 2^-53.
Line 2Because the total is one step short, floatTotal === 1 is false, which is why a decimal accumulator makes an unreliable loop or equality condition.
Line 3The integer loop adds 10 ten times; whole numbers this small are exact in a double, so 100 is exact.
Line 4Dividing by 100 only once, at the very end, keeps the single unavoidable rounding away from the accumulation.
Grouping changes the answer
Shows that floating-point addition is not associative, and that some intermediate results happen to be exact.
const a = 0.1, b = 0.2, c = 0.3;
console.log((a + b) + c);
console.log(a + (b + c));
console.log((a + b) + c === a + (b + c));
console.log(b + c === 0.5);Example explained
Line 1The stored doubles for 0.2 and 0.3 add up to exactly 0.5, a power-of-two fraction, so the right-hand grouping starts from an error-free intermediate.
Line 2a + b is already one step above the closest double to 0.3, and adding 0.3 to it rounds upward again, so the drift accumulates to 0.6000000000000001.
Line 3The === test is false even though both sides compute the same mathematical sum, which is why reordering a total can change its last digits.
Line 4Addition of doubles is still commutative: only the grouping, not the order of a single pair, matters.
Important notes
Number.EPSILON is the gap between 1 and the next representable double (2^-52), not a universal error bound; that gap doubles at every power of two, so at 1000 it is about 1.1e-13.
The console prints the shortest decimal that round-trips back to the stored double, so console.log(0.3) shows 0.3 even though the stored value is 0.2999999999999999888977697537484...; toFixed(20) reveals more of it.
Common mistakes
Treating 0.1 + 0.2 !== 0.3 as a JavaScript defect and patching it with toFixed, which returns a string: (0.1 + 0.2).toFixed(2) + 0.1 produces the string '0.300.1' instead of a number.
Reusing one hardcoded tolerance such as 0.0001 everywhere: it declares 1e-6 and 2e-6 equal, and near 1e12 the gap between neighbouring doubles is already bigger than 0.0001, so two values one rounding step apart are reported as different.
Keeping a running money balance in a float, so adding 0.1 ten times gives 0.9999999999999999 and a later comparison against the expected total, or a cents-level report, silently disagrees.
Try it yourself
Change, predict, then run
In a browser console, add 0.01 one hundred times in a loop, then log the total, total === 1, and total.toFixed(20). Redo the loop with an integer counter that adds 1 each time and divide by 100 at the end, and compare the two totals.
Open the JavaScript workspaceCheck your understanding
Why does 0.1 + 0.2 === 0.3 evaluate to false while 0.5 + 0.25 === 0.75 evaluates to true?
- 0.5, 0.25 and 0.75 are all fractions with power-of-two denominators, so each is stored exactly and their exact sum is representable; 0.1, 0.2 and 0.3 each become a slightly different nearby value
- JavaScript keeps more precision for numbers written with fewer decimal digits
- === applies a small built-in tolerance when both operands are halves or quarters
- 0.1 and 0.2 are stored exactly, but the addition operator introduces error that 0.5 + 0.25 avoids
Show answer
0.5, 0.25 and 0.75 are k / 2^n values, so nothing is rounded on the way in and nothing is rounded on the way out, and the comparison holds. The tempting answer is the one blaming the addition, but 0.1 and 0.2 are already approximations before the operator runs: addition of two doubles returns the correctly rounded result of the exact sum of what is stored, so the discrepancy originates in the literals, not in the arithmetic.