Crux Read four short JS snippets and predict what the runtime actually does: float rounding, the safe-integer limit, silent coercion, and the typeof null quirk.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min
The surprises in everyday code all trace back to the unit’s core idea: bits are stored under one type rule and later read under another. Read each snippet, predict the exact output, and name the rule that produced it.
Goal
Practise the loop you run whenever a program prints something unexpected: identify which type rule the runtime applied to the bits, and explain why the output is what it is rather than what you first guessed.
Heads-up Binary floating point cannot store 0.1 or 0.2 exactly, just as base-10 cannot store 1/3 exactly. The stored values are already slightly off, so the sum is 0.30000000000000004 and the equality is false.
Heads-up Floats add fine; the operation never errors. The catch is precision: the IEEE 754 result is 0.30000000000000004, not 0.3, so only the printed value surprises you.
Heads-up JS stores the IEEE 754 double, which is itself inexact for 0.1 and 0.2 — it does not keep an exact decimal behind the scenes. The stored value really is 0.30000000000000004.
Snippet 2 — the integer that stops counting
const big = 9007199254740991; // Number.MAX_SAFE_INTEGERconsole.log(big + 1); // ?console.log(big + 1 === big + 2); // ?
Quiz
Completed
What does this print, and what is the underlying cause?
Heads-up Beyond MAX_SAFE_INTEGER the double's 52 mantissa bits cannot separate consecutive integers, so big + 1 and big + 2 collapse to the same value and the comparison is true, not false.
Heads-up JS numbers do not wrap; the single double type has no fixed integer width to overflow. Instead the value loses precision — large integers round to the nearest representable double.
Heads-up The value stores fine — doubles reach ~1.8e308. What fails above 2^53 is integer exactness: the result is silently rounded, not rejected. Use BigInt when you need exact large integers.
What three lines print, and what rule explains the difference?
Heads-up The + operator is overloaded: if either side is a string it concatenates, so 5 + '5' is '55', not 10. Only - and * (no string meaning) force numeric coercion.
Heads-up JS is dynamically typed and coerces rather than throwing here. It silently converts operands to make each operator work — which is exactly why the wrong type can produce a wrong value with no error.
Heads-up Strings win only for +. For - and *, which have no string definition, JS coerces the string to a number, giving 0 and 10. The operator decides the coercion direction.
What does each line print, and which result is a historical quirk?
Heads-up typeof null does not return 'null'. Because of a bug in the original implementation it returns 'object', and that can never be fixed without breaking the web. The reliable check is x === null.
Heads-up null === undefined is false: strict equality compares value and type, and these are two distinct primitives. They are only loosely equal under ==.
Heads-up They are distinct primitives with distinct typeof results: 'object' for null (the quirk) and 'undefined' for undefined. Under === they are not equal.
Recap
Every surprise here is the same unit idea in a JS costume. Float rounding (0.1 + 0.2) and the safe-integer limit (above 2^53) both come from the single 64-bit IEEE 754 double having finite precision — there is no separate integer type to fall back on. The + operator concatenates with strings but coerces to numbers for - and *, so dynamic typing silently picks a type rule for you. And typeof null returns ‘object’ — a frozen historical bug. Read the operator and the storage format, and the output stops being a mystery.