A loop with var and setTimeout
A loop with var and setTimeout prints the same number three times because of two things at once: var creates a single variable for the whole loop, and the timer callbacks run only after the loop has finished. Both causes have to coincide: on their own, neither asynchrony nor var produces this behaviour.
Theory
TL;DR
varis function scoped: the loop creates oneifor the entire function, not a new one per iteration.- Every
setTimeoutcallback closes over the same variable. setTimeoutruns later: the callbacks go into the macrotask queue.- That queue is processed only once the current JavaScript stack has emptied.
- By the time the first timer fires, the loop is over and
iholds its final value (3, for instance). - The fixes are
let, an IIFE, the third argument ofsetTimeout, orbind.
Quick example
The symptom:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3
// 3
// 3Cause 1: var is function scoped
In for (var i = 0; i < 3; i++) { ... } a single i is created for the whole function (or for the global scope), not a new one per iteration. All three arrow functions passed to setTimeout close not over "the value of i at creation time" but over the binding itself of that one variable.
So once the loop ends, all three callbacks read the same memory slot, and it holds 3.
Cause 2: setTimeout runs later
setTimeout callbacks do not run immediately, not even with a delay of 0. They go into the macrotask queue and only start after the current JavaScript stack has emptied, that is, after the synchronous code (the whole loop included) has finished.
The order is:
- The loop runs to completion, calling
setTimeoutthree times and scheduling three callbacks. - The loop ends and
ibecomes3. - The stack empties, the event loop pulls the callbacks off the queue and runs them one by one.
- Each callback reads the current value of
i, which is already3.
How to fix it
Option 1: let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}The specification requires a for loop with let to create a fresh binding on every iteration and copy the previous value into it. Each callback then closes over its own copy.
Option 2: an IIFE, a closure that receives the current i
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 0); // 0, 1, 2
})(i);
}Calling a function creates a new scope, and the parameter j receives a copy of i as it was at call time.
Option 3: pass it as an argument to setTimeout
for (var i = 0; i < 3; i++) {
setTimeout(j => console.log(j), 0, i); // 0, 1, 2
}The third and later arguments of setTimeout are forwarded to the callback, and their values are captured when the timer is scheduled.
Option 4: bind
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 0); // 0, 1, 2
}bind returns a new function with the argument fixed up front, so it copies the value immediately as well.
Summary
| Option | How it captures the value |
|---|---|
let | A fresh binding on every loop iteration |
| IIFE | A new scope on every function call |
setTimeout argument | The value is handed to the callback at scheduling time |
bind | The argument is baked into a new function straight away |
In short:
vargives one "shared" variable for the whole loop, andsetTimeoutfires once the loop is already over. Uselet, or isolate the value with a closure or with arguments.
Common mistakes
- Blaming the delay and switching to
setTimeout(..., 100). The delay is irrelevant: the callback still runs after the loop. - Believing a closure copies a variable's value. A closure keeps a reference to the binding, not a snapshot of the value.
- Replacing
varwithletonly on a declaration inside the loop body rather than in theforheader. The per-iteration binding comes fromletin the header. - Using
varwithforEachand concluding the problem "went away by itself". It went away becauseforEachcalls a function per element, and that is already a separate scope. - Confusing this effect with hoisting. Hoisting explains why
iis visible before its declaration, not why every callback sees3.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.