Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~24 min· topic 3 of 6

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, 0 or 42.
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.

app.jswhole filejavascript
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.

app.jswhole filejavascript
0.1 + 0.2          // 0.30000000000000004
0.1 + 0.2 === 0.3  // false

03Compare 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.

app.jswhole filejavascript
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
const totalPaise = 10 + 20;  // 30, exact
console.log(totalPaise / 100); // 0.3

04Spot NaN and Infinity

Bad maths gives NaN, division by zero gives Infinity. Neither throws an error, so check results that come from user input.

app.jswhole filejavascript
const qty = Number("two");     // NaN
Number.isNaN(qty)             // true
10 / 0                        // Infinity
Number.isFinite(10 / 0)       // false

05Know where exact counting ends

Past Number.MAX_SAFE_INTEGER, the gaps between storable numbers become bigger than 1. Use BigInt there.

Know where exact counting endsdiagram
Rendering diagram…

Try it yourself

  1. 1

    Find another rounding error

    In the first example, add console.log(0.3 - 0.1);. Predict the output first. Is it exactly 0.2?

  2. 2

    Spread a NaN

    In the second example, add const total = 10 + Number("ten") + 5; and log total. Where did the NaN come from, and why didn't the program stop?

  3. 3

    Fix the mix

    In the BigInt example, change the last line to console.log(1n + BigInt(1));. What prints now?

Code & diagrams

Decimals, rounding errors and safe comparisons New tab

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
NaN, Infinity and negative zero New tab

`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
-Infinity
Where exact whole numbers end New tab

The 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
false
BigInt: exact integers of any size ES2020+ New tab

The 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 conversions

Break 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"); }

terminal
$ Console
── what you'll see ──
(nothing is printed)

Break #2

Add money as decimals

Show a cart total with const total = 0.1 + 0.2; el.textContent = "Total: " + total;

terminal
$ the page shows
── what you'll see ──
Total: 0.30000000000000004

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.parse turns 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.stringify throws TypeError: Do not know how to serialize a BigInt for BigInts.

  • ▸

    -0 appears in real code: Math.round(-0.4) is -0, and it survives multiplication. It prints as 0 in most places (String(-0) is "0"), so use Object.is(x, -0) to detect it.

  • ▸

    Number.EPSILON is 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. 1

    JavaScript has one number type for whole numbers (integers, like 42) and decimals (like 3.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. So 5 and 5.0 are the same number, and Number.isInteger(5.0) is true. You can write numbers as 1000, 1e3 (1 × 10³), 0xff (hexadecimal, 255), 0b101 (binary, 5) or 1_000_000 (the underscores, numeric separators from ES2021, are only for readability).

  2. 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.3 is false. 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, where Number.EPSILON is about 2.2e-16), and do money in whole cents (store 1999 paise, not 19.99 rupees), converting only for display. Topic 4.4 covers rounding and toFixed.

  3. 3

    NaN means "Not a Number": the result of a maths operation that has no sensible number answer, like 0 / 0, "apple" * 2 or Math.sqrt(-1). Oddly, its type is "number". NaN is the only value not equal to itself: NaN === NaN is false. So test with Number.isNaN(x). Avoid the older global isNaN(x): it converts its argument to a number first, so isNaN("hello") is true even though "hello" is a string, not NaN. Any arithmetic involving NaN gives NaN, so one bad value can spread silently through a calculation.

  4. 4

    Infinity and -Infinity are 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, beyond Number.MAX_VALUE, about 1.8e308). Division by zero does not throw an error in JavaScript. Number.isFinite(x) is true only for ordinary numbers (not NaN or ±Infinity). There is also a negative zero, -0: it equals 0 with ===, but 1 / -0 is -Infinity.

  5. 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 as Number.MAX_SAFE_INTEGER (and Number.MIN_SAFE_INTEGER for the negative side). Above it, numbers start skipping: 2 ** 53 + 1 gives 9007199254740992. 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. 6

    BigInt (ES2020) stores whole numbers of any size exactly. Write an n after the digits (9007199254740993n) or call BigInt("9007199254740993"). BigInts can't hold decimals (7n / 2n is 3n, the fraction is dropped), and you can't mix them with ordinary numbers in arithmetic: 1n + 1 throws a TypeError. Convert explicitly with Number(big) or BigInt(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

01

Why does 0.1 + 0.2 not equal 0.3 in JavaScript, and how do you handle money?

02

What is NaN, why is NaN === NaN false, and how do you test for it?

03

What is Number.MAX_SAFE_INTEGER and what happens beyond it?

04

When would you use BigInt, and what are its limits?

Practice

01

Add the prices 19.99 and 5.01 so that the result is exactly 25 and log true when you compare it with 25.

02

Log whether Number("12px") is NaN using the correct test, and log the result of 100 / 0.

03

Log 2 to the power of 70 exactly.

Trade-offs

  • ↔

    number is 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.2 isn't 0.3.

  • I can test for NaN and Infinity correctly.

  • 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.