Classic Closure-in-Loop Bug (var vs let)
debt(d3/e1/b3/t7)
Closest to 'default linter catches the common case' (d3). The detection_hints list ESLint and TypeScript, both mainstream default-or-near-default tools. ESLint's no-loop-func rule flags closures capturing var in loops without requiring specialist configuration, matching d3 precisely.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix explicitly states: replace var with let in the for loop declaration. This is a single keyword change that structurally eliminates the bug, matching e1 exactly.
Closest to 'localised tax' (b3). The bug applies per-loop in web and CLI contexts but doesn't impose a cross-cutting architectural cost. Each instance is isolated to a specific loop; fixing one doesn't require touching unrelated code. The choice of var vs let is local in scope.
Closest to 'serious trap' (t7). The misconception field states the bug is widely believed to apply only to setTimeout but actually affects any closure over a var-bound variable — event listeners, array callbacks, API call handlers. This contradicts the intuitive mental model that the loop variable 'belongs' to each iteration, a belief reinforced by experience with languages that have block-scoped loop variables by default, justifying t7.
Also Known As
TL;DR
Explanation
When you create closures inside a for loop with var, all closures share the same variable binding. After the loop finishes, all closures see the final value. Example: for (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 0); } prints 5 five times, not 0-4. The fix is to use let which creates a new binding per iteration. PHP developers are confused by this because PHP closures capture variables explicitly with use($i) which creates a copy by default.
Common Misconception
Why It Matters
Common Mistakes
- Using var instead of let in for loops with callbacks
- Attaching event listeners in a loop expecting each to see a different index
- Assuming Array.forEach avoids this — it does because its callback has its own scope
Code Examples
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
// Prints: 3, 3, 3 — not 0, 1, 2
// Fix 1: let (block-scoped):
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// Fix 2: closure capture with IIFE:
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}