The var and closures pitfall in a loop
The classic trap: in a loop declared with var, every callback sees the same i, because var is function scoped. By the time those callbacks run, the loop has already finished and i holds its final value, so instead of the expected 0, 1, 2 the console prints 3 three times.
Theory
TL;DR
varis function scoped: the entire loop shares exactly one variable.- A closure captures the variable, not the value it had when the function was created.
- An asynchronous callback reads that variable after the loop is over, when it holds the final value.
letin theforheader creates a fresh binding per iteration and the problem disappears.- If
varhas to stay, freeze the value with an IIFE, a factory,forEach, or asetTimeoutargument.
Quick example
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3Why it happens
The var i declaration is hoisted to the top of the function (or to the global scope), so there is a single variable shared by all three iterations. Each callback passed to setTimeout closes over a reference to that one variable instead of copying its value.
Then the event loop kicks in: even with a 0 delay the callback goes into the macrotask queue and runs only after the synchronous code, that is, the whole loop, has completed. At that moment i < 3 is already false because i === 3. All three callbacks read the same slot and print 3.
Fix 1: let or const
In ES6 a for (let i = ...) header gets a separate lexical binding on every iteration: the engine copies the current value into a new variable before the next step. Each callback then closes over its own variable:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}This is the simplest and safest route, and it is exactly why modern code does not use var in loops. For a for...of loop whose element is never reassigned, const works just as well.
Fix 2: freeze the value by hand
If you really do need var, the current iteration's value has to be moved into its own variable.
An IIFE (immediately invoked function expression) creates a new scope and freezes the current i:
for (var i = 0; i < 3; i++) {
(function(iCopy) {
setTimeout(() => console.log(iCopy), 0);
})(i);
}A function factory does the same thing more readably: the parameter x lives in the private environment of each created function:
function makeLogger(x) {
return () => console.log(x);
}
for (var i = 0; i < 3; i++) {
setTimeout(makeLogger(i), 0);
}Array methods (forEach, map) remove the question entirely: the callback parameter is already a separate binding per element:
[0, 1, 2].forEach(i => setTimeout(() => console.log(i), 0));The third argument of setTimeout passes the value straight into the callback (supported in browsers and in Node.js):
for (var i = 0; i < 3; i++) {
setTimeout(x => console.log(x), 0, i);
}Short checklist
- By default use
letorconstin loops, it is the simplest and safest path. - If
varis unavoidable, freeze the value with an IIFE, a factory, or an explicit argument. - Remember: closures capture the variable, not its instantaneous value, and with
varthere is only one variable for the whole loop.
Common mistakes
- Blaming
setTimeout. The timer is irrelevant: the same thing happens with any deferred call, event handler or promise callback. - Reading a
0delay as "immediately". The callback still goes to the queue and runs after the synchronous code. - Replacing
varwithletonly inside the loop body. The per-iteration binding comes fromletin theforheader, not from some variable declared deeper in the block (though a copy inside the block works too). - Using
varinfor...inandfor...ofwith callbacks. The trap is identical, even though the values look "fresh". - Confusing this with
this. The problem here is purely variable scope; the call site context has nothing to do with it.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.