Frontend Labs
⋯

56. JavaScript puzzles: predict the output, then explain it

This chapter is the guide's fun one. For each puzzle, cover the answer, predict it, and only then read on. Every answer was checked in Chromium 153. If you can explain all 16, you understand coercion, scope, this, the event loop and the standard library better than most interviewers.

Puzzle 1. Coercion.

[] + {};          [] == ![];          [] + [] === "";

"[object Object]", true, true. + with an object calls ToPrimitive (§53), and arrays and objects turn into strings ("" and "[object Object]"). ![] is false because objects are truthy. Then [] == false → "" == 0 → 0 == 0. This is why === exists (§3).

Puzzle 2. Floating point.

0.1 + 0.2 === 0.3;

false. The sum is 0.30000000000000004, because 0.1 has no exact binary representation. Compare with a tolerance (Math.abs(a - b) < Number.EPSILON), store money in integer cents, or use Math.sumPrecise for sums (§55).

Puzzle 3. NaN and friends.

typeof null;  typeof NaN;  NaN === NaN;  Object.is(NaN, NaN);  [NaN].includes(NaN);  [NaN].indexOf(NaN);

"object" (a bug from 1995 that can never be fixed), "number", false, true, true, -1. includes uses SameValueZero, which treats NaN as equal to itself. indexOf uses ===, which doesn't.

Puzzle 4. Closures in loops.

for (var i = 0; i < 3; i++) setTimeout(() => log(i));
for (let j = 0; j < 3; j++) setTimeout(() => log(j));

3 3 3 0 1 2. var is function-scoped, so all three callbacks share one i, which is 3 by the time any timer fires (§12). let creates a fresh binding for every iteration (§4).

Puzzle 5. Losing this.

const user = { name: "Ada", greet() { return `hi ${this.name}`; } };
const { greet } = user;
greet();

In a module (strict mode), this is undefined, so this throws a TypeError. In a sloppy script it is window, and you get "hi " (because window.name is ""). this depends on how a function is called, not where it is defined (§6). Fix it with user.greet.bind(user) or an arrow function in a class field.

Puzzle 6. map(parseInt).

["1", "7", "11"].map(parseInt);

[1, NaN, 3]. map passes (value, index), so the calls are parseInt("1", 0) (radix 0 means "default" → 1), parseInt("7", 1) (no such base → NaN) and parseInt("11", 2) (binary → 3). Use .map(Number).

Puzzle 7. Default sort.

[10, 9, 1].sort();

[1, 10, 9]. Without a comparator, sort compares strings. Use (a, b) => a - b, and prefer toSorted (§55) so the original isn't mutated.

Puzzle 8. typeof and the TDZ.

typeof notDeclaredAnywhere;      // ?
{ typeof tdz; let tdz = 1; }     // ?

"undefined", then a ReferenceError. typeof is safe on undeclared names but not on a let that is declared and still in its temporal dead zone (§4).

Puzzle 9. Spreading a class instance.

class T { #c = 21; get double() { return this.#c * 2; } }
({ ...new T() }).double;

undefined. Spread copies own enumerable properties. Getters live on the prototype, and #private fields are not properties at all. The same thing happens with structuredClone and JSON.stringify.

Puzzle 10. Holes.

Array(3).map(() => 1);
Array.from({ length: 3 }, () => 1);

[ <3 empty items> ] and [1, 1, 1]. Array(3) has a length but no elements, and map skips holes. Array.from treats the length as real indexes.

Puzzle 11. What JSON drops.

JSON.stringify({ a: undefined, b: () => 1, c: NaN, d: new Date(0), e: [undefined] });

{"c":null,"d":"1970-01-01T00:00:00.000Z","e":[null]}. undefined and functions disappear from objects but become null in arrays. NaN becomes null. Dates go through toJSON(). Silent data loss over an API often comes from exactly this.

Puzzle 12. || vs ??.

0 || "default";   0 ?? "default";   "" || "x";   null ?? "x";

"default", 0, "x", "x". || falls back on any falsy value, ?? only on null/undefined. That is the bug behind "volume 0 resets to 50".

Puzzle 13. await inside forEach.

[30, 10, 20].forEach(async (ms) => { await wait(ms); log(ms); });
log("after forEach");

after forEach 10 20 30. forEach ignores the promises its callback returns, so nothing waits. Use for...of with await (one after another) or await Promise.all(items.map(...)) (in parallel).

Puzzle 14. Cloning a class.

class Point { constructor(x) { this.x = x; } norm() { return Math.abs(this.x); } }
const c = structuredClone(new Point(-3));
c instanceof Point;  c.x;  typeof c.norm;

false, -3, "undefined". Structured clone (used by postMessage, IndexedDB and structuredClone) copies data, not prototypes (§38). Rehydrate on the other side: Object.assign(new Point(), c).

Puzzle 15. Aliasing.

const a = [1, 2, 3]; const b = a; b.length = 0;
a;

[]. b is the same array. Writing length truncates it. const protects the binding, not the value.

Puzzle 16. Promise.all fails fast.

await Promise.all([Promise.reject(new Error("x")), new Promise(() => {})]);

It rejects immediately with x, even though the second promise never settles. Use Promise.allSettled when you need every outcome, and Promise.any for "first success wins".

🎯 Event-loop puzzles, live. The Browser Lab capstone (§81) turns puzzles like these into an interactive quiz. You predict the order of the logs, step through the call stack and queues, then run the real code and compare.


Generated from real-life-JS-guide.md · 8 Oct 2026 16:36