JavaScriptClosuresLoopsScope

Why var and let Behave Differently in for Loops

Use the classic JavaScript closure example to understand why var in a for loop prints 3 3 3, while let creates per-iteration bindings and prints 0 1 2.

·Updated ·7 min read·Counting...
Why var and let Behave Differently in for Loops

The classic var vs let loop difference is not about one keyword being old and the other being new. It is about whether closures capture one shared variable or a new binding for each iteration.

The classic example

function createCounters() {
  const counters = []

  for (var i = 0; i < 3; i++) {
    counters.push(function () {
      return i
    })
  }

  return counters
}

Call it:

const counters = createCounters()

console.log(counters[0]())
console.log(counters[1]())
console.log(counters[2]())

Output:

3
3
3

Why var gives 3 3 3

Closures do not copy the number at the moment the function is created. They keep access to the variable binding.

var is function-scoped, so the loop has one shared i. After the loop finishes, that one i is 3. Every function reads the same final variable.

Why let gives 0 1 2

function createCounters() {
  const counters = []

  for (let i = 0; i < 3; i++) {
    counters.push(function () {
      return i
    })
  }

  return counters
}

Output:

0
1
2

let creates a separate binding for each loop iteration. Each closure keeps the binding from its own iteration.

The real lesson

Do not memorize "let is better" as a magic phrase. Remember the binding model:

  • var: one function-scoped variable
  • let: block-scoped variable, with a fresh binding per loop iteration in this pattern

The difference appears anywhere callbacks outlive the loop body.

Where this bug appears

Common places:

  • callbacks created in for loops
  • setTimeout
  • event handlers
  • async tasks
  • promise callbacks

Example:

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

This prints 3 three times.

IIFE workaround

Older code used IIFE to create a new scope:

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

The function receives the current value as j. In modern code, prefer let unless you are maintaining legacy compatibility.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.