Numbers and BigInt
Mental model: Every number is a 64 bit float, so precision is finite and you plan for it.
Level: beginner · about 12 minutes
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.3 - 0.1); // 0.19999999999999998The most reported non-bug in the language.
JavaScript has one number type: an IEEE-754 double, 64 bits, the same format C and Java call double. It stores values in binary, and 0.1 has no exact binary representation, exactly as 1/3 has no exact decimal one. You are seeing the rounding, not a bug.
Comparing with a tolerance
const nearlyEqual = (a, b, eps = Number.EPSILON) => Math.abs(a - b) < eps;
console.log(Number.EPSILON); // 2.220446049250313e-16
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true
console.log(nearlyEqual(1, 1.0000001)); // false
Where integers stop being safe
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 (2**53 - 1)
console.log(9007199254740992 === 9007199254740993); // true, they collapse
console.log(Number.isSafeInteger(2 ** 53)); // false
const big = 9007199254740993n; // the n suffix makes it a BigInt
console.log(typeof big); // 'bigint'
console.log(big + 1n); // 9007199254740994n
try {
console.log(big + 1); // mixing is deliberate friction
} catch (err) {
console.log(err.name); // 'TypeError'
}BigInt: exact integers, any size, and no mixing.
- Use
BigIntfor database ids, cryptography, and counters that can exceed 2^53. - Do not use it for money: it has no fractions, and
10n / 3ntruncates to3n. JSON.stringifythrows on a BigInt, so serialise it as a string yourself.- Comparisons across types still work:
10n == 10is true,10n === 10is false.
Infinity and NaN
console.log(1 / 0, -1 / 0); // Infinity -Infinity
console.log(0 / 0); // NaN
console.log(NaN === NaN); // false, the only value unequal to itself
console.log(Number.isNaN('abc')); // false, it does not convert
console.log(isNaN('abc')); // true, the old global does convert
Turning text into numbers
| Expression | Result | Behaviour |
|---|---|---|
Number('42px') | NaN | all or nothing |
parseInt('42px', 10) | 42 | reads a prefix, stops at the first bad character |
parseFloat('3.9kg') | 3.9 | like parseInt, but keeps decimals |
Number('') | 0 | the surprise: an empty string becomes zero |
+' 12 ' | 12 | unary plus, same rules as Number |
parseInt('08') | 8 | safe today, but always pass the radix: parseInt(s, 10) |
console.log(Number('42'), Number('42px'), Number('')); // 42 NaN 0
console.log(parseInt('42px', 10), parseFloat('3.9kg')); // 42 3.9
console.log(+' 7 ', Number(true), Number(null)); // 7 1 0
console.log(Number(undefined)); // NaN
Formatting, and the toFixed trap
console.log((1.005).toFixed(2)); // '1.00', because 1.005 is really 1.00499...
console.log(typeof (1.5).toFixed(1)); // 'string', not a number
console.log((1234.5678).toFixed(2)); // '1234.57'
console.log(1_000_000 + 1); // 1000001, separators are just readability
`Number.isInteger(x)`- true for 4 and 4.0, false for 4.5 and for
"4" `Number.isFinite(x)`- no conversion, so
Number.isFinite("4")is false `Number.isNaN(x)`- no conversion, unlike the global
isNaN `Number.isSafeInteger(x)`- integer and within plus or minus 2^53 - 1
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);Past 2^53 the representable numbers are two apart, so both sums round to the same double, 9007199254740992. That is what "safe integer" means: below the limit, every integer has its own representation and arithmetic is exact. Above it, use BigInt.
Try it yourself
Money without floats
// Work in pence, convert only when you display.
const pence = (pounds) => Math.round(pounds * 100);
const format = (p) => `\u00a3${(p / 100).toFixed(2)}`;
const basket = [19.99, 4.5, 0.1, 0.2];
const total = basket.reduce((sum, item) => sum + pence(item), 0);
console.log('float total:', basket.reduce((s, n) => s + n, 0)); // 24.790000000000003
console.log('integer total:', total, format(total));
Add a 20 percent tax step. Then try the same total with plain floats and compare the last digits.
Exercises
Compare floats safely
Write nearlyEqual(a, b, epsilon = Number.EPSILON) which returns true when the two numbers are within epsilon of each other, and false otherwise. It must return a boolean, and NaN must never be close to anything.
A stricter toNumber
Write toNumber(value) that converts a value to a number, but refuses partial text. For strings: trim first, and return NaN if what is left is empty. Anything else goes through Number. So "12px" is NaN, and "" is NaN rather than 0.
Check yourself
- What does this log?
- 0 —
Numbertreats an empty or whitespace-only string as0, so the sum is0. This is the conversion that silently turns a blank form field into a real zero, which is why a stricter wrapper is worth writing. - Which of these returns a number rather than a string?
- Number('1.5') —
toFixedformats for display and always returns a string, which is the classic source of"1.50" + 1 === "1.501". If you need a number back, convert again withNumber(...)or a unary+. - You need exact arithmetic on integers larger than 2^53. What do you reach for?
BigInt—BigIntstores integers exactly at any size. The cost is that you cannot mix it withnumberin arithmetic, it has no fractions, andJSON.stringifyrefuses it, so ids usually travel as strings.
Common mistakes
- Comparing floats with
===, especially money. Use integer minor units or a tolerance. - Using the global
isNaN, which converts first, instead ofNumber.isNaN. - Forgetting
toFixedreturns a string, then concatenating instead of adding.
Takeaways
- There is one
numbertype: a 64 bit IEEE-754 double, so precision is finite. - Compare floats with a tolerance, or avoid the problem by storing integer minor units.
NaNis a number and is unequal to itself. Detect it withNumber.isNaN.Numberis all or nothing,parseIntandparseFloatread a prefix, andtoFixedreturns a string.- Integers are exact up to 2^53 - 1. Beyond that, use
BigIntand accept that it does not mix.