Number Precision and IEEE 754
Why 0.1 + 0.2 !== 0.3, safe integers, rounding strategies, and when to use integers, BigInt, or decimal libraries.
- javascript
- number-precision
JavaScript’s Number is IEEE 754 double-precision binary floating point (64-bit). That gives a huge range and fractional values — and classic surprises like money math that doesn’t add up.
0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004
Not a bug in V8. Binary fractions cannot represent 0.1 exactly, same as 1/3 in decimal.
Mental model
- 1 sign bit, 11 exponent bits, 52 mantissa bits (+ implicit leading 1)
- Integers are exact only in a range:
Number.MIN_SAFE_INTEGER…Number.MAX_SAFE_INTEGER(±(2⁵³−1)) - Outside that range, integers start skipping (you can’t represent every integer)
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true — both round to same
Number.isSafeInteger(9007199254740993); // false
Comparisons that work
function nearlyEqual(a, b, eps = Number.EPSILON) {
return Math.abs(a - b) < eps;
}
// For larger magnitudes, use relative epsilon:
function nearlyEqualRel(a, b, rel = 1e-9) {
return Math.abs(a - b) <= rel * Math.max(1, Math.abs(a), Math.abs(b));
}
Number.EPSILON is the difference between 1 and the next representable number — fine near 1, wrong as a universal money tolerance.
Money: don’t use raw floats
// Prefer integer minor units
const priceCents = 1999; // $19.99
const taxCents = Math.round(priceCents * 0.08);
const totalCents = priceCents + taxCents;
function formatUSD(cents) {
return (cents / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
});
}
Or a decimal library / BigInt for fixed-scale arithmetic. Never accumulate floats for ledgers.
Rounding APIs
Math.round(1.5); // 2 — half away from -∞ for positives… know banker's rounding is NOT default
Math.floor(-1.2); // -2
Math.trunc(-1.2); // -1
(1.005).toFixed(2); // "1.00" in many engines — string, and still FP quirks
For UI display, format once at the edge with Intl.NumberFormat. For correctness, round at defined business steps in integer space.
Parsing and precision
Number('0.1');
parseFloat('0.1px'); // 0.1 — stops at non-num
parseInt('08', 10); // always pass radix
0.1 + 0.2 + 0.3; // left-assoc FP path ≠ 0.6 exactly
BigInt when integers exceed safe range
const id = 9007199254740993n;
id + 1n; // exact
// Cannot mix Number and BigInt with +
IDs from backends as large integers should arrive as strings or BigInt, not Number.
Interview answer (out loud)
“JS numbers are IEEE 754 doubles. Many decimals aren’t exact in binary, so 0.1+0.2 isn’t 0.3. Integers are safe only up to 2^53−1. For money I use integer cents or a decimal type; for comparisons I use an epsilon or relative tolerance; for huge ints I use BigInt or strings.”
Multiplication order
0.1 * 3; // 0.30000000000000004
(0.1 * 10 * 3) / 10; // sometimes used as a trick — still not a money system
Integer scaling remains the robust approach.
isFinite vs Number.isFinite
isFinite('10'); // true — coerces
Number.isFinite('10'); // false
Number.isFinite(10); // true
Prefer Number.isFinite / Number.isNaN for type-safe checks.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.