Five Handlers, One Counter: var, let, and the Temporal Dead Zone
A dashboard renders 5 KPI cards. Each card gets a click handler that should log its index 0-4. The handler is registered in a for (var i = 0; i < cards.length; i++) loop using setTimeout(() => console.log(i), 0), and every card logs 5. A second file defines const format = (n) => n.toFixed(1) at the bottom of the module, but a top-level call format(ratio) runs before that line and throws ReferenceError: Cannot access 'format' before initialization only in the build where the top-level call exists. A third helper is written as function sum(a, b) { return a + b } and is called from line 3 of the same file without error. The team uses ES modules, strict mode, and no transpiler. Two developers disagree about whether var or let is 'hoisted' and whether moving the const above the call is enough.
1. Explain why every card logs 5, and give three fixes ranked by risk.
Read the worked answer
var i is a single function-scoped binding shared by all 5 iterations. The arrow function closes over that one variable, not over a copy of its value. The setTimeout callbacks run after the loop finishes, when i is already 5, so each callback logs 5 - and as a click index, 5 is out of range for a 5-item list. Fixes: declare the counter with let, which creates a fresh per-iteration binding (smallest change, no behavioural side effects); capture the value with an IIFE (function (idx) { /* ... */ })(i); or, for real DOM handlers, read Number(el.dataset.index) from the element rather than closing over anything. A fourth option, setTimeout(fn, 0, i), also works because extra arguments are passed to the callback. This is about bindings, not about setTimeout being special: any callback that runs after the loop shows the same symptom.
2. Compare what happens when `var`, `const`, and a function declaration are used before their definition line.
Read the worked answer
All three declarations are hoisted, but they are initialised differently. A function declaration's binding is created and initialised with the function object before any code runs, so the line-3 call succeeds. A var binding is created and initialised to undefined, so early use gives undefined rather than an error - often a silent bug. A let or const binding is created but left uninitialised in the temporal dead zone; any read or write before the declaration line throws ReferenceError: Cannot access 'format' before initialization. So moving const format = ... above the call is a genuine fix, but only within the same scope. In modules, which are always strict, a function declaration inside a block is block-scoped and invisible outside it, unlike sloppy-mode function-in-block behaviour. Both developers are partly right: hoisting happens for all, but only var and function declarations are usable before their line.
3. A teammate suggests 'just pass `i` to `setTimeout`'. Does that work, and what are the caveats?
Read the worked answer
Yes, setTimeout(callback, 0, i) works: extra arguments are passed to the callback when it runs, so each scheduled callback receives the value i held at scheduling time, producing 0-4. The value is copied when setTimeout is called, which is exactly what you want. Two caveats. It fails if the callback ignores its parameters: setTimeout(() => console.log(i), 0, i) still reads the shared outer i, and the third argument is unused. And using setTimeout purely as a scoping trick is the wrong tool for a click handler, since real handlers are not scheduled that way - use let, a closure factory, or dataset. Reserve setTimeout(..., 0) for deliberately deferring work to a later task. Add a comment if you rely on argument passing, because it looks like a typo to reviewers.