What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

If callbacks created in a JavaScript loop all log the same value, they are probably reading the same variable binding after the loop has changed it. The usual fix for a counter loop is to declare let in the for initializer; for collections, for...of or forEach can give each callback its own iteration item.

Why do callbacks from a loop log the same value?

A closure lets a function access bindings in the surrounding lexical environment. When a loop creates callbacks that run later, those callbacks may share one changing binding. By the time they execute, the loop may have advanced or finished, so each callback reads the same final value.

For example, in for (var i = 0; i < 3; i++), var does not create a separate counter binding for each pass. If each pass schedules a delayed callback that logs i, all three callbacks can run after the loop and log 3. MDN illustrates this behavior in its JavaScript for statement reference and its closures guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do you fix a JavaScript setTimeout loop?

Declare the counter with let in the loop initializer

For a numeric counter that each delayed callback must remember, put let directly in the for initializer:

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// Logs 0, 1, 2

A let declaration in the for initializer receives per-iteration bindings that callbacks can observe. The placement matters: it is not enough to use the let keyword if the variable is declared once outside the loop.

Use for...of when callbacks need collection items

If you are iterating over a collection and the callback needs the current item rather than a numeric index, use a per-iteration item binding:

for (const item of items) {
  schedule(() => console.log(item));
}

const prevents reassignment of that iteration’s binding; it does not prevent mutation of an object referenced by the binding.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use forEach when callback-based iteration fits

For collection work that suits the array callback pattern, forEach is another option:

items.forEach((item) => {
  schedule(() => console.log(item));
});

MDN documents for...of and forEach as alternatives to the classic var-based closure pattern in its closures guide.

For legacy var code, capture the current value in a function scope

If older code must retain var, pass the current item into a function scope so the callback closes over that value:

for (var i = 0; i < items.length; i++) {
  (function (item) {
    schedule(() => console.log(item));
  })(items[i]);
}

This is a documented workaround for code written before block-scoped let and const.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why can let still produce the same value?

let is block-scoped, but callbacks still share a binding if the variable is declared only once outside the loop and then updated repeatedly:

let i = 0;
for (; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// Logs 3, 3, 3

Move the declaration into the for initializer to get a binding for each iteration, or capture the current item in a per-iteration binding. MDN shows the shared-binding case alongside the per-iteration behavior in its for statement reference.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Which fix should you choose?

  • Numeric counter needed: use let in the for initializer.
  • Collection item needed: choose for...of, or forEach if its callback-based iteration fits your code.
  • Legacy code must keep var: create a function scope that receives the current value.
  • Callback runs synchronously: it may run before the loop changes the binding, so the delayed-callback failure pattern may not apply. Check when the callback actually executes.

These are alternatives for different code shapes, not a single rule for every codebase. The key question is whether each callback needs a distinct iteration value or whether sharing one changing binding is intended. For more on the language fundamentals behind these patterns, see MDN’s JavaScript language overview.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.