JavaScript Challenges That Trump the Normal Developer

Here are some fun JavaScript challenges that look simple but often stump developers. Each one exploits a quirk of the language—closures, this, type coercion, or the event loop. Try to predict the output (or fix the bug) before peeking at the answers.
Challenge 1: The Closure Loop Trap
What does this log?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Answer
3, 3, 3 — not 0, 1, 2.
var is function-scoped, so there’s only one i. By the time the timeouts run, the loop has finished and i is 3. Fix: use let (block-scoped) so each iteration gets its own i, or pass i into an IIFE/setTimeout(..., i).
Challenge 2: this in a Method
What does this log?
const obj = {
name: "JS",
greet() {
console.log(this.name);
},
};
const greet = obj.greet;
greet();
Answer
undefined (or in sloppy mode in a browser, possibly the global object’s name).
this is determined by how the function is called. greet() is called with no receiver, so this is undefined in strict mode (or the global in sloppy mode). The function wasn’t “taken from” obj in a way that preserves this. Fix: obj.greet() or greet.call(obj).
Challenge 3: Loose Equality Coercion
What does this log?
console.log([] + []);
console.log([] + {});
console.log({} + []);
console.log(true + true + true === 3);
console.log("5" - 3);
console.log(`5${3}`);
Answer
[] + []→""(arrays become""viatoString(), so"" + "").[] + {}→"[object Object]"(array →"", object →"[object Object]").{} + []→0in some environments:{}can be parsed as an empty block, so+[]is evaluated;+[]coerces to0.true + true + true === 3→true(booleans coerce to numbers: 1 + 1 + 1 === 3)."5" - 3→2(-triggers numeric coercion)."5" + 3→"53"(+with a string does string concatenation).
Challenge 4: The Event Loop Order
What’s the log order?
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Answer
A, D, C, B.
Sync code runs first (A, D). Then the microtask queue (promises) runs before the macrotask queue (setTimeout), so C then B. Many developers guess B before C.
Challenge 5: Object Reference vs “Copy”
What does this log?
const a = { x: 1 };
const b = a;
b.x = 2;
console.log(a.x);
Answer
2.
b and a point to the same object. Mutating b.x mutates the shared object, so a.x is also 2. “Copy” would require something like const b = { ...a } or structuredClone(a).
Challenge 6: Optional Chaining + Nullish
What does this log?
const obj = { a: 0, b: "", c: null, d: undefined };
console.log(obj?.e ?? "missing");
console.log(obj?.a ?? "missing");
console.log(obj?.b ?? "missing");
Answer
obj?.e ?? "missing"→"missing"(eis undefined, so nullish → fallback).obj?.a ?? "missing"→0(??only falls back fornull/undefined;0is kept).obj?.b ?? "missing"→""(same idea; empty string is not nullish).
Challenge 7: Array Sort “In Place”
What does this log?
const arr = [10, 5, 20, 1];
arr.sort();
console.log(arr);
Answer
[1, 10, 20, 5] — not numeric order.
.sort() with no comparator sorts lexicographically (as strings). So "10" comes before "5". Fix: arr.sort((a, b) => a - b).
Challenge 8: parseInt with a Radix
What does this log?
console.log(Number.parseInt("08"));
console.log(Number.parseInt("08", 10));
console.log(["1", "2", "3"].map(Number.parseInt));
Answer
parseInt("08")— historically some engines treated leading0as octal; in modern JS it’s usually8(treated as decimal). Best practice: always pass radix.parseInt("08", 10)→8.["1", "2", "3"].map(parseInt)→[1, NaN, NaN].mappasses(element, index);parseInttakes a second argument as radix. So you getparseInt("1", 0),parseInt("2", 1),parseInt("3", 2)— only the first is valid. Fix:.map((s) => parseInt(s, 10))or.map(Number).
Challenge 9: Hoisting and Temporal Dead Zone
What does this log?
console.log(foo);
var foo = "bar";
console.log(baz);
let baz = "qux";
Answer
- First line:
undefined—var foois hoisted and initialized withundefined; the assignment runs later. - Third line: ReferenceError —
let bazis in the TDZ until the declaration runs, so accessing it throws.
Challenge 10: typeof and Undeclared
What does this log?
console.log(typeof x);
console.log(typeof neverDeclared);
Answer
- If
xwas never declared:"undefined"—typeofis safe and returns"undefined"for undeclared identifiers (no ReferenceError). - Same for
neverDeclared:"undefined".
Use Them Wisely
These aren’t meant to gatekeep—they’re a reminder that JavaScript’s “weird parts” (scoping, this, coercion, event loop) can bite you in real code. Use let/const, avoid relying on == coercion, pass radix to parseInt, and remember: microtasks before macrotasks.
If you’ve got a favorite JS gotcha or a different solution, share it.
Thanks for reading!
If you found this helpful, consider sharing it with others.