← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Classic Closure-in-Loop Bug (var vs let)

javascript ES2015 Intermediate
debt(d3/e1/b3/t7)
d3 Detectability Operational debt — how invisible misuse is to your safety net

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.

e1 Effort Remediation debt — work required to fix once spotted

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.

b3 Burden Structural debt — long-term weight of choosing wrong

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.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

Also Known As

var in loop closure loop loop closure problem

TL;DR

The most common JavaScript closure bug: using var in a for loop captures the loop variable by reference so all callbacks see the final value — fixed by using let which creates a new binding per iteration.

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

The loop bug only applies to setTimeout — it applies to any closure in a for/while loop capturing a var variable, including event listeners and array callbacks.

Why It Matters

Async callbacks in loops are ubiquitous in event-driven code — this bug causes wrong values in event handlers, API calls, and UI updates.

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

✗ Vulnerable
for (var i = 0; i < 3; i++) {
    setTimeout(function() { console.log(i); }, 100);
}
// Prints: 3, 3, 3 — not 0, 1, 2
✓ Fixed
// 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);
}

Added 22 Mar 2026
Edited 23 Mar 2026
Views 27
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 1 ping T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F
No pings yet today
No pings yesterday
Amazonbot 10 Google 4 Perplexity 4 Unknown AI 3 Ahrefs 3 Majestic 1
crawler 21 crawler_json 3 pre-tracking 1
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Low
⚡ Quick Fix
Replace var with let in all for loops that contain closures — let creates a new binding per iteration making the bug structurally impossible
📦 Applies To
javascript ES2015 web cli
🔗 Prerequisites
🔍 Detection Hints
for(var i = ... in loop with callback addEventListener or setTimeout; all event handlers logging same index value
Auto-detectable: ✓ Yes eslint typescript
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Low ✓ Auto-fixable Fix: Low Context: Function

✓ schema.org compliant