The classic var vs let loop difference is not about one keyword being old and the other being new. It is about whether closures capture one shared variable or a new binding for each iteration.
The classic example
function createCounters() {
const counters = []
for (var i = 0; i < 3; i++) {
counters.push(function () {
return i
})
}
return counters
}
Call it:
const counters = createCounters()
console.log(counters[0]())
console.log(counters[1]())
console.log(counters[2]())
Output:
3
3
3
Why var gives 3 3 3
Closures do not copy the number at the moment the function is created. They keep access to the variable binding.
var is function-scoped, so the loop has one shared i. After the loop finishes, that one i is 3. Every function reads the same final variable.
Why let gives 0 1 2
function createCounters() {
const counters = []
for (let i = 0; i < 3; i++) {
counters.push(function () {
return i
})
}
return counters
}
Output:
0
1
2
let creates a separate binding for each loop iteration. Each closure keeps the binding from its own iteration.
The real lesson
Do not memorize "let is better" as a magic phrase. Remember the binding model:
var: one function-scoped variablelet: block-scoped variable, with a fresh binding per loop iteration in this pattern
The difference appears anywhere callbacks outlive the loop body.
Where this bug appears
Common places:
- callbacks created in
forloops setTimeout- event handlers
- async tasks
- promise callbacks
Example:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
This prints 3 three times.
IIFE workaround
Older code used IIFE to create a new scope:
for (var i = 0; i < 3; i++) {
;(function (j) {
setTimeout(() => console.log(j), 0)
})(i)
}
The function receives the current value as j. In modern code, prefer let unless you are maintaining legacy compatibility.
Related FreeMac guides
- For the broader scope difference, read JavaScript var vs let: Function Scope, Block Scope, and Closures.
- For arrays and loops, read JavaScript Array Methods: map, splice, sort, and for...of.
- For timers and microtasks, read JavaScript Event Loop: Tasks, Microtasks, and Rendering.
Continue reading
JavaScript var vs let: Function Scope, Block Scope, and Closures
Understand the difference between var and let in JavaScript through function scope, block scope, for loops, closures, and why let avoids the classic 3 3 3 result.
12 min readJavaScript Event Loop: Tasks, Microtasks, and Rendering
Understand the browser event loop through the call stack, tasks, microtasks, Promise callbacks, queueMicrotask, async/await, rendering opportunities, and long tasks.
13 min readJavaScript Array Methods: map, splice, sort, and for...of
Choose JavaScript array methods by mutation, return value, early exit, async behavior, and readability: map, filter, reduce, find, slice, splice, sort, toSorted, and for...of.
Subscribe to FreeMac
Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.