If delayed callbacks created inside a JavaScript loop all log the same value, they are usually reading the same changing var binding after the loop has advanced or finished. For a counter loop, put let in the for initializer so each iteration gets a binding of its own. For collection iteration, for...of or forEach can make the per-item value clearer.
Why the callbacks see the same value
A closure lets a function access bindings in the surrounding environment. It does not necessarily save a frozen copy of every variable’s value when the function is created. If several callbacks close over one binding and that binding changes before they run, they read its later value.
As an Amazon Associate I earn from qualifying purchases.
For example, with for (var i = 0; i < 3; i++), var does not create a new loop-local binding for each pass. If each pass schedules a delayed callback that logs i, the loop can finish before any callback runs. All three callbacks then read the same binding, whose value is 3, and log 3. MDN demonstrates this behavior in its JavaScript for statement reference and closures guide.
Free tools Windows power users keep installed
One-click scans. No signup required.
This is about when the callback runs, not only which loop syntax appears in the code. A callback that runs immediately, before the counter changes, can observe a different value from one that runs later, such as a callback passed to setTimeout.
#1 Best Overall
Choose a fix that matches what the callback needs
| Situation | Suitable approach | Why it helps |
|---|---|---|
| A delayed callback needs the numeric counter from each pass | Declare let in the for initializer |
Each iteration has a binding for closures to observe. |
| A delayed callback needs each value from a collection | Use for...of with const or let |
The callback closes over that iteration’s item binding. |
| Callback-based collection iteration fits the task | Use forEach |
Each callback receives the current item as an argument. |
Legacy code must keep a var loop |
Capture the current item in a per-iteration function scope | The callback closes over a function parameter rather than the shared counter. |
Fix a counter loop with let
Declare let i in the loop initializer, rather than declaring it once outside and updating that same variable. In a for loop, JavaScript creates a per-iteration binding that callbacks can observe.
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Logs 0, 1, 2
The delay in this example is one second; the important point is that the callbacks run later and each sees its own iteration’s counter. MDN explains the special per-iteration behavior of let in the for initializer in its reference.
Rank #2
Use an iteration value for collection callbacks
for...of
When the callback needs the current item rather than a numeric index, iterate over the items directly:
for (const item of items) {
schedule(() => console.log(item));
}
Each pass binds item for that iteration. Use const when the binding itself should not be reassigned; it does not prevent mutation of an object that the binding refers to.
forEach
If a callback-based iteration suits the surrounding code, the item can be passed directly to the forEach callback:
items.forEach((item) => {
schedule(() => console.log(item));
});
Both patterns are documented alternatives to the shared-var-binding pattern in MDN’s closures guide. Pick based on whether the code needs a counter, an item, or the callback form already used by the surrounding code.
Rank #4
Keep var in legacy code by capturing the current value
If the loop must retain var, create a function scope on each pass and pass the current item into it. The delayed callback then closes over the function parameter, which does not change when the loop counter advances.
for (var i = 0; i < items.length; i++) {
(function (item) {
schedule(() => console.log(item));
})(items[i]);
}
This pattern is a workaround for code written before block-scoped let and const; MDN describes it in the closures guide.
Best Value
Check where the variable is declared
Changing the keyword to let does not automatically create a new binding per iteration. This still uses one shared variable because i is declared outside the loop:
let i = 0;
for (; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
The callbacks can all log 3. Move the declaration into the initializer—for (let i = 0; ...)—or capture the current item in a per-iteration binding. The declaration’s location is what changes the behavior in this case, as MDN’s loop reference illustrates.
Quick Recap
How to diagnose the issue in your code
- Check whether the callback runs later. Look for scheduling functions or other asynchronous work. If the callback runs after the loop has continued, it may observe a later value.
- Find the binding the callback reads. Check whether it is a
vardeclared in the loop or a variable declared once outside and updated by the loop. - Decide whether the callback needs an index or an item. Use
letin the counter loop initializer for a per-iteration index; use an item-oriented pattern when the item is what matters. - Apply the matching fix and check the outputs. For the three-pass delayed counter example, per-iteration bindings produce
0,1, and2, rather than three copies of the final counter.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.




