Topic 1.3
Numbers in Depth
In one line
Every JavaScript number is a 64-bit floating-point value, which is why 0.1 + 0.2 isn't exactly 0.3, why NaN and Infinity exist, and why whole numbers above 2^53 − 1 need BigInt.
Think of it like this
Writing one-third on a calculator. You get 0.3333333, and the calculator stops at some digit, so 0.3333333 × 3 gives 0.9999999, not 1. Computers store numbers in binary (only 0s and 1s), and in binary, simple decimals like 0.1 are like one-third: they go on forever and must be cut off. That tiny cut is why 0.1 + 0.2 comes out as 0.30000000000000004.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Integer
- A whole number with no fraction, like
-3,0or42. - Floating point
- A way of storing numbers with a movable decimal point. Fast, but most decimals are stored as close approximations.
- IEEE 754
- The international standard for floating-point numbers that JavaScript (and most languages) follow.
- Binary
- Counting with only two digits, 0 and 1. Computers store everything this way.
- NaN
- "Not a Number": the result of a maths operation with no sensible answer, like
0 / 0. - Infinity
- A number value bigger than every other number, for example the result of
1 / 0. - Overflow
- When a result is too big to store, so it becomes
Infinity. - MAX_SAFE_INTEGER
9007199254740991, the largest whole number a normal number can store exactly.- BigInt
- A separate type for exact whole numbers of any size, written like
10n.
Step by step
01One type for all numbers
Whole numbers and decimals share the number type. Different ways of writing a number still make the same value.
5 === 5.0 // true
1e3 // 1000
0xff // 255
1_000_000 // 1000000 (separators are ignored)
7 / 2 // 3.5 (division never rounds)02See the rounding error
0.1 and 0.2 are each stored as the nearest binary fraction. Adding the two tiny errors produces a result that is the nearest double to 0.30000000000000004, not to 0.3.
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false03Compare and count money safely
Compare with a tolerance, or avoid fractions entirely by working in whole cents. Whole numbers up to 2^53 − 1 are always exact.
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
const totalPaise = 10 + 20; // 30, exact
console.log(totalPaise / 100); // 0.304Spot NaN and Infinity
Bad maths gives NaN, division by zero gives Infinity. Neither throws an error, so check results that come from user input.
const qty = Number("two"); // NaN
Number.isNaN(qty) // true
10 / 0 // Infinity
Number.isFinite(10 / 0) // false05Know where exact counting ends
Past Number.MAX_SAFE_INTEGER, the gaps between storable numbers become bigger than 1. Use BigInt there.
Try it yourself
- 1
Find another rounding error
In the first example, add
console.log(0.3 - 0.1);. Predict the output first. Is it exactly0.2? - 2
Spread a NaN
In the second example, add
const total = 10 + Number("ten") + 5;and logtotal. Where did the NaN come from, and why didn't the program stop? - 3
Fix the mix
In the BigInt example, change the last line to
console.log(1n + BigInt(1));. What prints now?
Code & diagrams
Numeric separators (`1_000_000`) arrived in ES2021.
Console
(nothing was printed)Expected console output
3.5
0.30000000000000004
false
true
20
1000000 1000 255 5
true`Number.isNaN` and `Number.isFinite` arrived in ES2015 and don't convert their argument; the old global `isNaN` does.
Console
(nothing was printed)Expected console output
NaN
NaN
number
false
true
true false
NaN
Infinity -Infinity
Infinity
false
true
-InfinityThe literal `9007199254740993` can't be stored, so it is rounded to the nearest storable number before your code even runs.
Console
(nothing was printed)Expected console output
9007199254740991
9007199254740992
9007199254740992
9007199254740992
true
falseThe last line throws on purpose: BigInt and number can't be mixed in arithmetic.
Console
(nothing was printed)Expected console output
18446744073709551616n
9007199254740994n
3n
15n
false true
11
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversionsBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Check for NaN with ===
Write const age = Number("abc"); if (age === NaN) { console.log("bad age"); }
Break #2
Add money as decimals
Show a cart total with const total = 0.1 + 0.2; el.textContent = "Total: " + total;
Myth vs fact
Myth
0.1 + 0.2 !== 0.3 is a JavaScript bug.
Fact
It's how IEEE 754 binary floating point works in nearly every language. Python prints 0.30000000000000004 too.
Myth
Dividing by zero crashes the program.
Fact
In JavaScript it gives Infinity, -Infinity or (for 0 / 0) NaN. No error is thrown, so you must check results yourself.
Myth
isNaN(x) tells you whether x is NaN.
Fact
The global isNaN converts first, so isNaN("hello") is true. Number.isNaN(x) is true only for the actual NaN value.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
V8 stores small integers ("Smis", 31 or 32 bits depending on the platform) directly in a tagged value, without allocating a heap number. Arithmetic that stays in that range is very fast; overflowing it or introducing decimals switches to heap-allocated doubles.
- ▸
JSON.parseturns large integer IDs into doubles, corrupting anything above 2^53 − 1. Ask APIs to send big IDs as strings, or parse them with a reviver.JSON.stringifythrowsTypeError: Do not know how to serialize a BigIntfor BigInts. - ▸
-0appears in real code:Math.round(-0.4)is-0, and it survives multiplication. It prints as0in most places (String(-0)is"0"), so useObject.is(x, -0)to detect it. - ▸
Number.EPSILONis the gap between 1 and the next double; it's a good tolerance only for values near 1. For larger values scale it:Math.abs(a - b) <= Number.EPSILON * Math.max(Math.abs(a), Math.abs(b)).
Remember this
- 1
JavaScript has one
numbertype for whole numbers (integers, like42) and decimals (like3.14). Every number is stored as a double-precision floating-point value, a format defined by the IEEE 754 standard: 64 bits holding a sign, an exponent (where the decimal point goes) and 52 bits of digits. So5and5.0are the same number, andNumber.isInteger(5.0)istrue. You can write numbers as1000,1e3(1 × 10³),0xff(hexadecimal, 255),0b101(binary, 5) or1_000_000(the underscores, numeric separators from ES2021, are only for readability). - 2
Because of the binary cut-off, most decimal fractions are stored slightly wrong, and the errors can show up after arithmetic:
0.1 + 0.2 === 0.3isfalse. This is not a JavaScript bug; Python, Java and C behave the same. Two safe habits: compare decimals with a small tolerance (Math.abs(a - b) < Number.EPSILON, whereNumber.EPSILONis about2.2e-16), and do money in whole cents (store1999paise, not19.99rupees), converting only for display. Topic 4.4 covers rounding andtoFixed. - 3
NaNmeans "Not a Number": the result of a maths operation that has no sensible number answer, like0 / 0,"apple" * 2orMath.sqrt(-1). Oddly, its type is"number".NaNis the only value not equal to itself:NaN === NaNisfalse. So test withNumber.isNaN(x). Avoid the older globalisNaN(x): it converts its argument to a number first, soisNaN("hello")istrueeven though"hello"is a string, notNaN. Any arithmetic involvingNaNgivesNaN, so one bad value can spread silently through a calculation. - 4
Infinityand-Infinityare real number values. You get them by dividing a non-zero number by zero (1 / 0), or when a result is too large to store (overflow, beyondNumber.MAX_VALUE, about1.8e308). Division by zero does not throw an error in JavaScript.Number.isFinite(x)istrueonly for ordinary numbers (notNaNor ±Infinity). There is also a negative zero,-0: it equals0with===, but1 / -0is-Infinity. - 5
With 52 bits of digits (plus one hidden bit), a double can count every whole number exactly only up to 2^53 − 1 =
9007199254740991, available asNumber.MAX_SAFE_INTEGER(andNumber.MIN_SAFE_INTEGERfor the negative side). Above it, numbers start skipping:2 ** 53 + 1gives9007199254740992. Large IDs from a server (like tweet IDs) can be silently corrupted this way, which is why APIs send them as strings.Number.isSafeInteger(x)tells you if a whole number is in the safe range. - 6
BigInt (ES2020) stores whole numbers of any size exactly. Write an
nafter the digits (9007199254740993n) or callBigInt("9007199254740993"). BigInts can't hold decimals (7n / 2nis3n, the fraction is dropped), and you can't mix them with ordinary numbers in arithmetic:1n + 1throws a TypeError. Convert explicitly withNumber(big)orBigInt(num). BigInts are slower than numbers, so use them only when you really need exact huge integers (IDs, cryptography, exact counters).
Explain it without notes
Why does 0.1 + 0.2 not equal 0.3 in JavaScript, and how do you handle money?
What is NaN, why is NaN === NaN false, and how do you test for it?
What is Number.MAX_SAFE_INTEGER and what happens beyond it?
When would you use BigInt, and what are its limits?
Practice
Add the prices 19.99 and 5.01 so that the result is exactly 25 and log true when you compare it with 25.
Log whether Number("12px") is NaN using the correct test, and log the result of 100 / 0.
Log 2 to the power of 70 exactly.
Trade-offs
- ↔
numberis fast and fits almost everything; BigInt is exact for huge integers but slower, can't hold decimals and needs explicit conversion everywhere it meets normal numbers. - ↔
Integer cents are simple and exact for adding prices, but percentages and currency conversion still produce fractions; decide where you round, and do it once.
Done when you can
I can explain why
0.1 + 0.2isn't0.3.I can test for
NaNandInfinitycorrectly.I know the value and meaning of
Number.MAX_SAFE_INTEGER.I can create and use BigInts and know they can't mix with numbers.
I handle money in whole cents or paise.