Floating-Point Precision
JavaScript has a single numeric type for non-integers: the IEEE 754 double-precision binary float. That format cannot represent most decimal fractions exactly, which is why 0.1 + 0.2 famously does not equal 0.3. Understanding why this happens — and the standard patterns for working around it — is essential for anything involving money, measurements, or precise rounding.
Why 0.1 + 0.2 !== 0.3
Numbers are stored in base-2. Just as 1/3 has no finite decimal representation (0.3333…), values like 0.1 and 0.2 have no finite binary representation. They get rounded to the nearest representable 64-bit value, and those tiny rounding errors accumulate when you add them.
console.log(0.1 + 0.2); // not 0.3
console.log(0.1 + 0.2 === 0.3); // false
console.log((0.1 + 0.2).toFixed(20));
Output:
0.30000000000000004
false
0.30000000000000004441
The result is off by about 4.4e-17. This is not a bug in JavaScript — every language using IEEE 754 doubles (Java’s double, C’s double, Python’s float) behaves the same way.
Integers are safe up to
Number.MAX_SAFE_INTEGER(2^53 − 1). The precision problem is specifically about fractional decimal values, not whole numbers within that range.
Comparing floats with Number.EPSILON
Because exact === comparison is unreliable for computed floats, compare with a small tolerance. Number.EPSILON is the smallest difference between 1 and the next representable number (~2.22e-16) and makes a sensible baseline tolerance.
const nearlyEqual = (a, b, epsilon = Number.EPSILON) =>
Math.abs(a - b) < epsilon;
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true
For larger magnitudes the absolute error grows, so scale the tolerance relative to the values being compared:
const closeEnough = (a, b) =>
Math.abs(a - b) <= Number.EPSILON * Math.max(Math.abs(a), Math.abs(b), 1);
console.log(closeEnough(1000000.1 + 0.2, 1000000.3)); // true
Rounding strategies
Rounding is the most common fix when you only need to display a value at a fixed precision.
| Approach | Returns | Best for |
|---|---|---|
(n).toFixed(d) | string | Display with exactly d decimals |
Math.round(n * 10**d) / 10**d | number | Rounding for further math |
Intl.NumberFormat | string | Locale-aware display, currency |
(n).toPrecision(p) | string | Significant figures |
const round = (n, d = 2) => Math.round(n * 10 ** d) / 10 ** d;
console.log(round(0.1 + 0.2)); // 0.3
console.log((2.5).toFixed(0)); // "3"
console.log((1.005).toFixed(2)); // "1.00" ← surprise!
Output:
0.3
3
1.00
The last line is a classic gotcha: 1.005 is actually stored as 1.00499999…, so it rounds down. There is no clever rounding trick that fully escapes this — the value was already imprecise before you rounded it.
Never store currency as a float and round only at the end. The errors are already baked in. Use integers or a decimal library from the start.
Integer cents for money
The simplest robust pattern for money is to work entirely in the smallest unit (cents, pennies, satoshis) as integers, and only convert to a display string at the boundary.
// Store and compute in integer cents — never floats.
const addCents = (...amounts) => amounts.reduce((sum, c) => sum + c, 0);
const subtotal = addCents(1099, 250, 4999); // $10.99 + $2.50 + $49.99
console.log(subtotal); // 8348 cents
const format = (cents, locale = "en-US", currency = "USD") =>
new Intl.NumberFormat(locale, { style: "currency", currency })
.format(cents / 100);
console.log(format(subtotal)); // "$83.48"
Output:
8348
$83.48
Addition, subtraction, and multiplication-by-integer are all exact in this scheme. The only place you touch a float is the final cents / 100 divide, immediately consumed by Intl.NumberFormat for display.
Decimal libraries
When you need division, tax rates, interest, or arbitrary precision, reach for a dedicated decimal library. These represent numbers in base-10 internally, so decimal fractions are exact.
import Decimal from "decimal.js";
const total = new Decimal("0.1").plus("0.2");
console.log(total.equals("0.3")); // true
console.log(total.toString()); // "0.3"
const price = new Decimal("19.99");
const tax = price.times("0.0825");
console.log(tax.toDecimalPlaces(2).toString()); // "1.65"
| Library | Notes |
|---|---|
decimal.js | Arbitrary precision, configurable rounding modes |
big.js | Tiny, minimal API for basic arithmetic |
bignumber.js | Like decimal.js, widely used in finance/crypto |
For whole-number values larger than Number.MAX_SAFE_INTEGER, the built-in BigInt type is the right tool — but note it holds integers only, not decimals.
Best Practices
- Never compare computed floats with
===; use a tolerance based onNumber.EPSILON. - Store money as integer cents and convert to a display string only at the edge.
- Use
Intl.NumberFormatfor currency and locale-aware formatting instead of manual string building. - Reach for a decimal library (
decimal.js,big.js) when you need exact division or percentages. - Remember
toFixedreturns a string and can round counterintuitively ((1.005).toFixed(2)). - Round at the display layer, not during intermediate calculations, to avoid compounding error.
- Use
BigIntfor large integers, but never expect it to handle fractional values.