The Infamous Loop Closure Bug
MedThe loop closure bug is one of the most famous JavaScript interview questions. When creating closures inside a loop with var, all closures share the same variable reference. When the callbacks execute, they all see the final value of that variable. Understanding why this happens (var scoping vs block scoping) and how to fix it is essential.
Interactive Visualization
CodeCreation Phase
1function outer() {2 let x = 10;3 function inner() {4 return x;5 }6 return inner;7}89const fn = outer();10fn(); // 10
Call Stack
Global EC
Variables:
fnundefined
Heap Memory
(empty)
Output
—
Global EC created - outer function is defined
1 / 6
Key Insight: A closure is formed when an inner function is returned from an outer function, maintaining access to the outer function's variables even after the outer function has completed.
Key Points
- var is function-scoped, not block-scoped
- All closures in a var loop share the same variable reference
- By the time callbacks run, the loop has finished (i is final value)
- Fix 1: Use let instead of var (block-scoped, new binding each iteration)
- Fix 2: Use an IIFE to create a new scope
- Fix 3: Use forEach (new function scope each iteration)
Code Examples
The Bug with var
for (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // Expected: 0, 1, 2 // Actual: 3, 3, 3 // Why? // 1. var i is function-scoped (one variable shared) // 2. Loop runs immediately, i becomes 3 // 3. setTimeout callbacks execute later // 4. All closures reference the SAME i (which is now 3)
var creates one variable for the entire function. All closures share this same reference.
Fix 1: Use let (Block Scope)
for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // Output: 0, 1, 2 ✅ // Why? // 1. let is block-scoped // 2. Each iteration creates a NEW binding of i // 3. Each closure captures its own i // 4. TDZ ensures proper initialization // JavaScript engine effectively does: // { // let i = 0; // setTimeout(() => console.log(i), 100); // } // { // let i = 1; // setTimeout(() => console.log(i), 100); // } // ...
let creates a new binding for each iteration, so each closure captures a different i.
Fix 2: IIFE (Old School)
for (var i = 0; i < 3; i++) { (function(capturedI) { setTimeout(() => { console.log(capturedI); }, 100); })(i); } // Output: 0, 1, 2 ✅ // The IIFE creates a new scope // capturedI is a parameter (new variable each call) // Closure captures capturedI, not the outer i
IIFE creates a new function scope, capturing the current value of i at each iteration.
Fix 3: forEach
[0, 1, 2].forEach(i => { setTimeout(() => { console.log(i); }, 100); }); // Output: 0, 1, 2 ✅ // forEach callback creates new scope // Each iteration has its own i parameter // No var scoping issues
forEach creates a new function scope for each iteration, avoiding the var scoping problem.
Common Mistakes
- Using var in loops with async operations or callbacks
- Not understanding why let fixes it
- Thinking the problem is with closures, not var scoping
Interview Tips
- This is the #1 closure interview question
- Explain both the problem (var scoping) and the solution (let)
- Know the IIFE fix for older code
- Understand TDZ helps with let in this case