Question

JavaScript Closures in Loops: How to Capture the Correct Loop Variable Value

javascriptloopsclosures

Question

In JavaScript, why do functions created inside a loop often all use the final value of the loop variable instead of the value from the iteration where they were created?

For example:

var funcs = [];

// Create 3 functions
for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value:", i);
  };
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}

This outputs:

My value: 3
My value: 3
My value: 3

But the desired output is:

My value: 0
My value: 1
My value: 2

The same issue appears with event listeners:

var buttons = document.getElementsByTagName("button");

for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", function() {
    console.log("My value:", i);
  });
}
<button>0</button>
<button>1</button>
<button>2</button>

It also happens in asynchronous code:

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  wait(i * 100).then(() => console.log(i));
}

And in for...in / for...of loops:

const arr = [1, 2, 3];
const fns = [];

for (var i in arr) {
  fns.push(() => console.log("index:", i));
}

for (var v of arr) {
  fns.push(() => console.log("value:", v));
}

for (const n of arr) {
  var obj = { number: n };
  fns.push(() => console.log("n:", n, "|", "obj:", JSON.stringify(obj)));
}

for (var f of fns) {
  f();
}

How can you ensure each closure captures the value from its own loop iteration?

Short Answer

By the end of this page, you will understand why closures inside JavaScript loops often all reference the same variable, why var causes this behavior, and how to fix it using let, function factories, or IIFEs. You will also see how this applies to event listeners, asynchronous code, and for...of / for...in loops.

Concept

A closure is a function that remembers variables from the scope where it was created. This is one of JavaScript’s most useful features.

The problem in loop examples is not that closures are broken. The problem is that with var, the loop variable is function-scoped, not block-scoped. That means every function created inside the loop closes over the same single variable, not a new variable for each iteration.

So in this code:

for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log(i);
  };
}

all the functions refer to the same i. After the loop finishes, i is 3, so every function prints 3.

Modern JavaScript solves this neatly with let. A let variable in a loop creates a new binding for each iteration. That means each closure gets its own value.

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log(i);
  };
}

Now each function remembers a different i.

This matters in real programming because closures are everywhere:

  • event listeners
  • timers
  • promises
  • callbacks
  • array methods
  • UI code

If you do not understand how variables are captured, you can easily introduce bugs that are hard to notice at first.

Mental Model

Imagine a loop variable as a whiteboard.

With var, there is one shared whiteboard. Each loop iteration erases it and writes a new number. Every function created in the loop only remembers where the whiteboard is, not what was written at that moment. When the functions run later, they all read the final value left on the board.

With let, each iteration gets its own small note card. Each function keeps the card from its own iteration. Later, when it runs, it reads the value from that card.

So:

  • var in loops -> one shared variable
  • let in loops -> a fresh variable per iteration
Take Quiz

Syntax and Examples

Preferred modern solution: let

const funcs = [];

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value:", i);
  };
}

funcs[0]();
funcs[1]();
funcs[2]();

Output:

My value: 0
My value: 1
My value: 2

Because let is block-scoped, each iteration gets its own i.

Function factory solution

If you must support older code styles, create a new function scope and pass the current value into it.

function makeLogger(value) {
  return function() {
    console.log("My value:", value);
  };
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = makeLogger(i);
}

funcs[0]();
funcs[1]();
funcs[2]();

Here, value is a separate parameter each time makeLogger(i) runs.

IIFE solution

An IIFE is an Immediately Invoked Function Expression.

var funcs = [];

for (var i = 0; i < 3; i++) {
  (function(currentValue) {
    funcs[currentValue] = function() {
      console.log("My value:", currentValue);
    };
  })(i);
}

This works for the same reason as the function factory: each call gets its own currentValue.

Event listener example with let

const buttons = document.getElementsByTagName("button");

for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", function() {
    console.log("My value:", i);
  });
}

Each listener remembers the correct button index.

Promise example with let

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

for (let i = 0; i < 3; i++) {
  wait(i * 100).then(() => console.log(i));
}

Output:

0
1
2

Each callback closes over a different i.

Step by Step Execution

Consider this example:

const funcs = [];

for (let i = 0; i < 3; i++) {
  funcs.push(() => console.log(i));
}

funcs[0]();
funcs[1]();
funcs[2]();

Step-by-step

  1. funcs is created as an empty array.
  2. First loop iteration:
    • i is 0
    • a function is created
    • that function closes over the iteration-specific i = 0
    • the function is pushed into funcs
  3. Second loop iteration:
    • a new i binding is created with value 1
    • a second function closes over i = 1
  4. Third loop iteration:
    • a new i binding is created with value 2
    • a third function closes over i = 2
  5. The loop ends.
  6. funcs[0]() logs 0
  7. funcs[1]() logs 1
  8. funcs[2]() logs 2

Now compare with var:

const funcs = [];

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

funcs[0]();
funcs[1]();
funcs[2]();

What changes?

  • There is only one i
  • Each function closes over that same i
  • After the loop, i is 3
  • All functions log 3

Real World Use Cases

Closures in loops show up in many practical situations.

1. Attaching event listeners

const items = document.querySelectorAll(".item");

for (let i = 0; i < items.length; i++) {
  items[i].addEventListener("click", () => {
    console.log("Clicked item", i);
  });
}

Used in menus, tabs, buttons, and lists.

2. Scheduling delayed actions

for (let i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(`Timer ${i} finished`);
  }, i * 1000);
}

Used in animations, retries, notifications, and delayed UI updates.

3. API request batching

const ids = [101, 102, 103];

for (let id of ids) {
  fetch(`/api/users/${id}`)
    .then((res) => res.json())
    .then((user) => console.log("Loaded user", id, user.name));
}

Each callback keeps the correct id.

4. Building reusable handlers

function createDeleteHandler(productId) {
  return () => console.log("Delete product", productId);
}

Common in frontend frameworks and admin dashboards.

Take Quiz

Real Codebase Usage

In real projects, developers usually avoid the old var loop problem by using modern patterns.

Common pattern: let in loops

for (let i = 0; i < rows.length; i++) {
  rows[i].onclick = () => openRow(i);
}

This is the most common and readable solution.

Common pattern: use the actual value instead of the index

Often you do not need the index at all.

for (const user of users) {
  sendEmail(user.email).then(() => {
    console.log("Sent email to", user.email);
  });
}

Using the item itself is often clearer than capturing i.

Common pattern: array methods

Developers often use forEach, map, or filter, which naturally give each callback its own parameters.

users.forEach((user, index) => {
  console.log(index, user.name);
});

Guard clauses and validation inside closures

buttons.forEach((button, index) => {
  button.addEventListener("click", () => {
    if (button.disabled) return;
    submitChoice(index);
  });
});

This combines correct variable capture with clean control flow.

Configuration and callback creation

function makeRequestHandler(endpoint) {
  return async function() {
    const response = await fetch(endpoint);
    console.log("Fetched from", endpoint, response.status);
  };
}

This pattern is everywhere in real codebases: creating closures that intentionally capture values.

Common Mistakes

1. Using var in a loop and expecting a new value each time

Broken code:

var funcs = [];

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

Why it fails:

  • var creates one shared variable
  • all closures use the same i

Fix:

for (let i = 0; i < 3; i++) {
  funcs.push(() => console.log(i));
}

2. Assuming async code copies values automatically

Broken code:

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

This prints 3 three times because the callbacks run later.

Fix:

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

3. Capturing a changing object reference

const fns = [];

for (const n of [1, 2, 3]) {
  var obj = { number: n };
  fns.push(() => console.log(obj.number));
}

Problem:

  • obj is declared with var
  • all closures share the same obj variable

Fix:

for (const n of [1, 2, 3]) {
  const obj = { number: n };
  fns.push(() => console.log(obj.number));
}

4. Using for...in on arrays without understanding it

for...in iterates over property names, usually string indexes.

for (let i in arr) {
  console.log(i); // "0", "1", "2"
}

If you want array values, prefer for...of:

for (let value of arr) {
  console.log(value);
}

5. Thinking arrow functions cause the problem

They do not. This issue is about variable scope, not arrow syntax.

These behave the same regarding closure capture:

() => console.log(i)
function() {
  console.log(i);
}

Comparisons

ApproachWorks correctly in loops?Best use caseNotes
varNo, not by itselfOlder code onlyOne shared function-scoped variable
letYesModern JavaScriptCreates a new binding per iteration
constYes in for...of / for...inWhen value should not be reassignedCannot be used as a normal incrementing counter like for (const i = 0; ...)
Function factoryYesExplicit closure creationGood when you want clear reusable logic
IIFEYesLegacy JavaScriptOlder workaround before let

var vs let

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

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let", j), 0);
}

Output:

var 3
var 3
var 3
let 0
let 1
let 2

for...in vs for...of

Loop typeIterates overCommon use
for...inproperty names / keysobjects, sometimes array indexes
for...ofiterable valuesarrays, strings, maps, sets

For arrays, for...of is usually clearer when you want values.

Take Quiz

Cheat Sheet

Quick rules

  • Closures remember variables from outer scope.
  • var is function-scoped, so loop closures usually share one variable.
  • let is block-scoped and creates a fresh loop binding per iteration.
  • const also works per iteration in for...of and for...in loops.
  • Async callbacks, event handlers, and timers often reveal closure bugs.

Safe patterns

Use let

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

Use a function factory

function makeFn(value) {
  return () => console.log(value);
}

Use an IIFE for older code

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

Common edge cases

  • for...in on arrays gives indexes, not values.
  • Arrow functions do not fix closure bugs by themselves.
  • If you capture an object declared with var, all closures may share it.
  • Prefer const or let over var in modern JavaScript.

Best default advice

If you are writing modern JavaScript, use let in loop counters and const for per-iteration values.

FAQ

Why do all functions print the last loop value in JavaScript?

Because with var, all functions share the same loop variable. After the loop ends, that variable contains its final value.

Does let create a new variable on every loop iteration?

Yes. In loop headers, let creates a fresh binding for each iteration, which is why closures capture the correct value.

Is this problem caused by arrow functions?

No. Both arrow functions and regular functions close over variables the same way. The issue is scope, especially var.

How do I fix closure issues in old JavaScript code?

Use an IIFE or a function factory to create a new scope and pass the current value into it.

Does this happen only with setTimeout?

No. It also happens with event listeners, promises, callbacks, and any function that runs later.

Should I use for...in for arrays?

Usually no. For arrays, prefer for...of, forEach, or a normal for loop, depending on whether you need values or indexes.

Can objects have the same problem as numbers in closures?

Yes. If multiple closures capture the same variable that later points to another object, they all see the latest reference.

What is the best modern solution?

Use let for loop variables and const for values that should not be reassigned.

Take Quiz

Mini Project

Description

Build a small JavaScript demo that creates buttons and attaches click handlers that log the correct button index and label. This project demonstrates how closures behave in loops and shows the safest modern way to capture per-iteration values.

Goal

Create a list of buttons where each click logs the correct index and text for that specific button.

Requirements

  • Create an array of at least three button labels.
  • Render one button for each label.
  • Attach a click handler to each button.
  • Make each handler log the correct index and label.
  • Use a closure-safe approach such as let or a function factory.
Take Quiz

Keep learning

Related questions

View more