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 callsToPrimitive(§53), and arrays and objects turn into strings (""and"[object Object]").![]isfalsebecause 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 is0.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 useMath.sumPrecisefor 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.includesuses SameValueZero, which treats NaN as equal to itself.indexOfuses===, 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.varis function-scoped, so all three callbacks share onei, which is 3 by the time any timer fires (§12).letcreates 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),
thisisundefined, so this throws aTypeError. In a sloppy script it iswindow, and you get"hi "(becausewindow.nameis"").thisdepends on how a function is called, not where it is defined (§6). Fix it withuser.greet.bind(user)or an arrow function in a class field.
Puzzle 6. map(parseInt).
["1", "7", "11"].map(parseInt);
[1, NaN, 3].mappasses(value, index), so the calls areparseInt("1", 0)(radix 0 means "default" → 1),parseInt("7", 1)(no such base → NaN) andparseInt("11", 2)(binary → 3). Use.map(Number).
Puzzle 7. Default sort.
[10, 9, 1].sort();
[1, 10, 9]. Without a comparator,sortcompares strings. Use(a, b) => a - b, and prefertoSorted(§55) so the original isn't mutated.
Puzzle 8. typeof and the TDZ.
typeof notDeclaredAnywhere; // ?
{ typeof tdz; let tdz = 1; } // ?
"undefined", then aReferenceError.typeofis safe on undeclared names but not on aletthat 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#privatefields are not properties at all. The same thing happens withstructuredCloneandJSON.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, andmapskips holes.Array.fromtreats 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]}.undefinedand functions disappear from objects but becomenullin arrays.NaNbecomesnull. Dates go throughtoJSON(). 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 onnull/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.forEachignores the promises its callback returns, so nothing waits. Usefor...ofwithawait(one after another) orawait 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 bypostMessage, IndexedDB andstructuredClone) 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;
[].bis the same array. Writinglengthtruncates it.constprotects 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. UsePromise.allSettledwhen you need every outcome, andPromise.anyfor "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.