Question
JavaScript Closures in Loops: How to Capture the Correct Loop Variable Value
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() {
.(i);
};
}
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:
varin loops -> one shared variableletin loops -> a fresh variable per iteration
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() {
.(, value);
};
}
funcs = [];
( i = ; i < ; i++) {
funcs[i] = (i);
}
funcs[]();
funcs[]();
funcs[]();
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
funcsis created as an empty array.- First loop iteration:
iis0- a function is created
- that function closes over the iteration-specific
i = 0 - the function is pushed into
funcs
- Second loop iteration:
- a new
ibinding is created with value1 - a second function closes over
i = 1
- a new
- Third loop iteration:
- a new
ibinding is created with value2
- a new
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];
( id ids) {
()
.( res.())
.( .(, id, user.));
}
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.
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:
varcreates 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.(i), );
}
Comparisons
| Approach | Works correctly in loops? | Best use case | Notes |
|---|---|---|---|
var | No, not by itself | Older code only | One shared function-scoped variable |
let | Yes | Modern JavaScript | Creates a new binding per iteration |
const | Yes in for...of / for...in | When value should not be reassigned | Cannot be used as a normal incrementing counter like for (const i = 0; ...) |
| Function factory | Yes | Explicit closure creation | Good when you want clear reusable logic |
Cheat Sheet
Quick rules
- Closures remember variables from outer scope.
varis function-scoped, so loop closures usually share one variable.letis block-scoped and creates a fresh loop binding per iteration.constalso works per iteration infor...ofandfor...inloops.- 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
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.
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
letor a function factory.
Keep learning
Related questions
Deep Cloning Objects in JavaScript: Methods, Trade-offs, and Best Practices
Learn how to deep clone objects in JavaScript, compare structuredClone, JSON methods, and recursive approaches with examples.
Get Screen, Page, and Browser Window Size in JavaScript
Learn how to get screen size, viewport size, page size, and scroll position in JavaScript across major browsers with clear examples.
How JavaScript Closures Work: A Beginner-Friendly Guide
Learn how JavaScript closures work with simple explanations, examples, common mistakes, and practical use cases for real code.