Common JavaScript Confusions
A collection of surprising JavaScript behaviors that trip up almost everyone at some point.
What is it?
Most of the time JavaScript behaves the way you'd expect. But it has a handful of well-known quirks — leftover design decisions from decades ago — that surprise even experienced developers the first time they hit them. Knowing them in advance turns a confusing bug into "oh, that's just how this works."
Explain like I'm 10
Think of these like the weird exceptions in English spelling — 'i before e except after c' has plenty of exceptions, and once you've been warned about them, they stop being confusing surprises and just become 'the known weird parts.'
Examples
A handful of classic surprises
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(NaN === NaN); // false
console.log(typeof null); // "object"
console.log([] + []); // "" (empty string)
console.log([1, 2] + [3, 4]); // "1,23,4"More surprises: closures in loops, and array holes
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 3, 3, 3 — not 0, 1, 2 (all three closures share the same "var i")
console.log([1, , 3].length); // 3 — the missing middle slot still countsUsing var in the loop means every callback closes over the exact same variable, which has already finished looping by the time the callbacks run — switching to let fixes it, since each iteration gets its own binding.
How it works
Each of these traces back to a specific rule: floating-point numbers are stored in binary and can't represent every decimal exactly; NaN is specified to compare unequal to everything including itself; typeof null returning "object" is a bug from JavaScript's very first version that was never fixed, to avoid breaking existing code; and + on non-numbers tries to convert its operands to primitives (often strings) before combining them.
Why does it exist?
These aren't bugs introduced by any particular program — they're consequences of decisions (or accidents) baked into the language itself, decades ago, that can't be changed without breaking the entire web. Learning them once means you recognize the pattern instantly instead of losing an hour to it in the future.
When to use it
Keep this list in mind whenever a result looks "obviously wrong" at a glance — comparing floating-point numbers for exact equality, checking for NaN, or relying on typeof for a null check. Recognizing these patterns is what turns a mysterious bug into an instant diagnosis.
When not to use it
You don't need to work around these preemptively everywhere — most code never touches floating-point precision or NaN in a way that matters. Add the specific safeguard (Number.isNaN, rounding, an explicit null check) only where the code actually depends on getting it right.
Common mistakes
Comparing floating-point calculations with
===instead of checking they're 'close enough' (within a small tolerance).Using
someValue === NaNto check for NaN — it will always be false; useNumber.isNaN(someValue)instead.Checking
typeof value === "object"to detect an object and forgetting thatnullpasses that check too.
Practice exercises
- Easy:
Predict, then verify, the result of
0.1 + 0.2 === 0.3. - Medium:
Write a function
isNullOrObject(value)that correctly distinguishesnullfrom a real object. - Hard:
Write a function
safeEquals(a, b)that correctly returns true forNaN, NaNwhile behaving like===for everything else.
Interview questions
Why doesn't `0.1 + 0.2` equal `0.3` exactly in JavaScript?
Numbers are stored as binary floating-point, and most decimal fractions (including 0.1 and 0.2) can't be represented exactly in binary — the tiny rounding error that results is why the sum comes out as 0.30000000000000004 instead of a clean 0.3.
Given floating-point imprecision, how should you actually compare two computed decimal numbers for "equality"?
Check that the difference between them is smaller than a small tolerance value (an "epsilon"), like Math.abs(a - b) < 0.00001, instead of using === directly on values that came from floating-point arithmetic.
Why is `NaN === NaN` false, and how do you correctly check whether a value is `NaN`?
NaN is specified to compare unequal to every value, including itself, so === can never confirm it. Number.isNaN(value) is the correct check — it doesn't rely on equality at all.
Why does `typeof null` return `"object"` even though `null` isn't really an object?
It's a bug baked into the very first version of JavaScript — an implementation detail where null and objects happened to share an internal type tag — that was never fixed because too much existing code would break if it changed.
Given that `typeof anArray` also returns `"object"`, how do you actually detect whether a value is specifically an array?
Use Array.isArray(value) — typeof can't tell an array apart from a plain object or any other non-primitive, since they're all "object" to it.
Why does `typeof someFunction` return `"function"` instead of `"object"`, given that functions are technically objects too?
typeof special-cases anything callable and reports it as "function" specifically, as a convenience, even though under the hood a function really is an object (with extra internal behavior for being invoked).
Predict the output: `console.log([] + []);`
"" (an empty string) — the + operator converts both operands to primitives when neither is a number, and converting an empty array to a string gives ""; concatenating two empty strings is still "".
Predict the output: `console.log([1, 2] + [3, 4]);`
"1,23,4" — each array is converted to a string first ("1,2" and "3,4"), and + then concatenates those two strings directly, with no space or separator inserted between them.
Predict the output: `console.log([] == false);`
true — == coerces both sides toward numbers when comparing an object and a boolean: false becomes 0, and the array is first converted to a primitive string "", then to the number 0, so 0 == 0 is true.
What's the difference between `null == undefined` and `null === undefined`?
null == undefined is true — they're specifically defined to loosely equal each other and nothing else. null === undefined is false, since === never coerces types, and null and undefined are different types.
Predict the output: `console.log("0" == false);` versus `console.log("0" === false);`
The == version is true: both sides get coerced toward numbers, false becomes 0 and "0" becomes 0, so they match. The === version is false, since a string and a boolean are never equal under strict equality regardless of their coerced values.
Predict the output: `console.log([1] == 1);`
true — [1] is first converted to a primitive, which stringifies to "1", and that's then converted to the number 1 for the numeric comparison against 1, so they end up equal.
Why is `{} === {}` always false, even when both object literals look identical?
=== on objects compares identity — whether both sides are literally the exact same object in memory — not the contents. Two separately created objects are always different references, no matter how identical their properties look.
Predict the output: `console.log([25, 1, 10, 2].sort());`
[1, 10, 2, 25] — the default .sort() converts every element to a string first and compares them lexicographically (character by character), not numerically, so "10" sorts before "2" because "1" comes before "2" as a character; a numeric compare function like (a, b) => a - b is needed to sort numbers correctly.
Does `Array.prototype.sort()` mutate the original array, or return a new one?
It sorts in place and mutates the original array, while also returning that same array — it doesn't create a copy, so code that needs to preserve the original order should copy first, e.g. [...arr].sort().
Why does `[NaN].indexOf(NaN)` return `-1`, and what should you use instead?
.indexOf() uses strict equality (===) internally to compare elements, and NaN === NaN is always false, so it can never "find" a NaN this way. .includes() uses a different comparison algorithm (SameValueZero) that treats NaN as equal to itself, so [NaN].includes(NaN) correctly returns true.
Predict the output: `for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); }` versus the same loop with `let` instead of `var`.
With var, it logs 3, 3, 3 — all three callbacks close over the exact same single i, which has already finished the loop (ending at 3) by the time any of them run. With let, it logs 0, 1, 2, because let creates a fresh binding of i for each iteration, so each callback captures its own separate value.
What is the "temporal dead zone" for `let` and `const`?
The span of code between the start of a scope and the actual let/const declaration line, during which the variable technically exists (it's been hoisted) but accessing it throws a ReferenceError instead of returning undefined — unlike var, which is initialized to undefined immediately.
Predict the output: `console.log(x); let x = 5;`
It throws a ReferenceError ("Cannot access 'x' before initialization") — x is in the temporal dead zone until its declaration line runs, so referencing it earlier fails loudly instead of silently returning undefined the way an equivalent var would.
How can forgetting `var`/`let`/`const` before an assignment accidentally create a global variable?
In non-strict ("sloppy") mode, assigning to a name that was never declared anywhere creates a brand-new property on the global object instead of throwing an error — silently leaking a variable into global scope, which strict mode (and modules, which are strict by default) disallows by throwing a ReferenceError instead.
Predict the output: `console.log("5" - 2); console.log("5" + 2);`
3 and "52" — - only makes sense numerically, so it always coerces both sides to numbers first, giving 5 - 2 = 3. + is overloaded for string concatenation, so when either side is a string, it concatenates instead of subtracting numerically, giving "5" + "2" = "52".
Why are `[]` and `{}` both truthy in an `if` check, even though they're both "empty"?
JavaScript's falsy values are a fixed, specific list — false, 0, -0, "", null, undefined, and NaN — and every object (arrays and plain objects included, empty or not) is truthy by definition, regardless of how much or how little data it actually holds.
Predict the output: `console.log(new Array(3).length); console.log(new Array(3));`
3 for the length, but the array itself is [ <3 empty items> ] — three actual holes, not three elements set to undefined. Methods like .forEach() and .map() skip holes entirely, which is a common surprise compared to an array explicitly filled with undefined (like Array(3).fill(undefined)).
Predict the output: `function f() { return { ok: true }; } console.log(f());`
undefined — JavaScript's automatic semicolon insertion silently inserts a semicolon right after return when it's immediately followed by a newline, turning it into return; on its own line, with the object literal on the next line becoming unreachable dead code.
Predict the output: `console.log(Object.keys({ b: 1, 2: "x", a: 3, 1: "y" }));`
["1", "2", "b", "a"] — property keys that look like non-negative integers are always iterated first, in ascending numeric order, regardless of insertion order; only after those do the remaining string keys appear, in their original insertion order.
Why has it historically been important to always pass an explicit radix to `parseInt`, like `parseInt(str, 10)`?
Without a radix, older JavaScript engines would guess the base from the string's format — a leading 0 could make parseInt("08") get interpreted as octal (base 8), where 8 isn't even a valid octal digit, producing surprising results. Modern engines default to base 10 without a leading 0x, but always passing the radix explicitly removes any ambiguity or reliance on that default.
Predict the output: `console.log("10" < "9");`
true — comparing two strings uses lexicographic (character-by-character) ordering, not numeric value, and the character "1" sorts before "9", so "10" is considered "less than" "9" as text, even though 10 is numerically larger.
Why can you call a function before its definition appears later in the same scope, if it's a function declaration but not if it's a function expression assigned to a `var`?
A function declaration is hoisted completely, body and all, so it's fully usable from the very top of its scope. A var holding a function expression is only hoisted as a name initialized to undefined — the actual function value isn't assigned until execution reaches that line, so calling it earlier throws ("is not a function"), not because the name is undefined but because it hasn't been assigned yet at that point.
Why doesn't `typeof someUndeclaredVariable` throw an error, while directly referencing `someUndeclaredVariable` does?
typeof is specifically designed to safely report "undefined" for a name that was never declared at all, as a legacy safety net for checking whether a global exists without risking a crash — but actually using that undeclared name in any other expression throws a ReferenceError immediately.
Why might `delete obj.someProperty` silently do nothing instead of removing the property or throwing?
If the property was defined as non-configurable (which many built-in and some explicitly-defined properties are), delete simply fails without throwing in non-strict mode — it returns false to signal failure, but code that ignores that return value never notices, and the property just stays put.