Skip to main content

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

  • var is function scoped: the loop creates one i for the entire function, not a new one per iteration.
  • Every setTimeout callback closes over the same variable.
  • setTimeout runs 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 i holds its final value (3, for instance).
  • The fixes are let, an IIFE, the third argument of setTimeout, or bind.

Quick example

The symptom:

javascript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 3 // 3 // 3

Cause 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:

  1. The loop runs to completion, calling setTimeout three times and scheduling three callbacks.
  2. The loop ends and i becomes 3.
  3. The stack empties, the event loop pulls the callbacks off the queue and runs them one by one.
  4. Each callback reads the current value of i, which is already 3.

How to fix it

Option 1: let (block scope)

javascript
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

javascript
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

javascript
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

javascript
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

OptionHow it captures the value
letA fresh binding on every loop iteration
IIFEA new scope on every function call
setTimeout argumentThe value is handed to the callback at scheduling time
bindThe argument is baked into a new function straight away

In short: var gives one "shared" variable for the whole loop, and setTimeout fires once the loop is already over. Use let, 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 var with let only on a declaration inside the loop body rather than in the for header. The per-iteration binding comes from let in the header.
  • Using var with forEach and concluding the problem "went away by itself". It went away because forEach calls a function per element, and that is already a separate scope.
  • Confusing this effect with hoisting. Hoisting explains why i is visible before its declaration, not why every callback sees 3.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.