← Home ← Codex ← DEBT ← Engine
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 118
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings F 1 ping S 2 pings S 1 ping M 1 ping T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 1 ping S 0 pings M 1 ping T 0 pings W 1 ping T 1 ping F 1 ping S 0 pings S 1 ping M 0 pings T 2 pings W 1 ping T 0 pings F 0 pings S
No pings yet today
No pings yesterday
PetalBot 14 Amazonbot 13 Ahrefs 9 SEMrush 8 Google 6 Bing 6 ChatGPT 5 Scrapy 5 Perplexity 4 Unknown AI 4 Sogou 3 Brave Search 2 Applebot 2 Majestic 1 Claude 1 Meta AI 1 Twitter/X 1
crawler 77 crawler_json 7 pre-tracking 1
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
Closure javascript A closure is a function that remembers variables from the place where it was created, even after that outer code has finished running.

Closures let you create private state, build factory functions, and write callbacks that remember context. Understanding them unlocks most JavaScript patterns—from simple event handlers to module systems.

💡 If your closure captures the wrong value in a loop, switch from var to let—let creates a fresh binding per iteration.

Ask Codex about Closure →
JavaScript javascript The programming language of the browser — it reads and modifies the page (the DOM), reacts to user events, and fetches data without reloading.

JavaScript is the only language browsers execute, so every interactive behaviour on the web goes through it. Its two defining traits — single-threaded event loop and loose typing (== coercion) — explain the majority of both its bugs and its design patterns.

💡 Default to const, use === always, and reach for let only when a value genuinely reassigns.

Ask Codex about JavaScript →
Loop general A control structure that repeats a block of code — for a counted number of times, while a condition holds, or once per item in a collection.

Loops are where programs spend their time and where the classic bugs live — infinite loops, off-by-one, and the async-in-loop closure trap. They are also the first place performance goes wrong: work that could happen once (a query, a DOM lookup) accidentally happens N times inside the loop body.

💡 Prefer foreach/for...of over manual counters — most loop bugs are counter bugs, and iterating the collection directly has no counter.

Ask Codex about Loop →
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
🤖 AI Agent
Confidence: High False Positives: Low ✓ Auto-fixable Fix: Low Context: Function


✓ schema.org compliant