Closures in Loops: How to Keep Your Sanity When Capturing Variables

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Does Your JavaScript Code Act Like a Game of Broken Telephone?

Imagine: you write a simple loop, add event handlers to buttons, and suddenly — bam! — all buttons show the same number. Sound familiar? If you've ever dealt with closures in a JavaScript loop, you know that moment of cognitive dissonance. You're sure the code is correct, but the browser stubbornly insists: "No, buddy, you messed something up."

On Stack Overflow, this question has gathered hundreds of thousands of views. And it's no wonder: capturing loop variables in a closure is one of the most common traps for beginners and even experienced developers. Let's figure out why this happens and, most importantly, how to fix it with performance in mind.



The Classic Problem: var and Asynchrony

Take a look at this innocent code:

for (var i = 0; i < 5; i++) {    setTimeout(function() {        console.log(i); // What will it output?    }, 100);}

If you answered "0, 1, 2, 3, 4" — congratulations, you're an optimist. Reality is harsh: the console will show 5, 5, 5, 5, 5. Why? Because var has function scope, not block scope. By the time setTimeout executes, the loop has already finished, and i will be 5. All five closures reference the same variable.

This is a classic example of the closure problem in JavaScript loops. And the solution is on the surface: you need to create a new scope for each iteration.



Solution #1: IIFE — The Good Old Crutch

Before ES6, developers used Immediately Invoked Function Expressions (IIFEs):

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

Here, j is a copy of i at the moment the IIFE is called. Each closure captures its own value. Does it work? Yes. Is it pretty? Not really. From a closure optimization standpoint, this creates an extra function on each iteration, which can impact memory if the loop is huge.



Solution #2: let — The Savior from ES6

With the arrival of let, the problem was solved elegantly:

for (let i = 0; i < 5; i++) {    setTimeout(function() {        console.log(i); // 0, 1, 2, 3, 4    }, 100);}

let creates a new binding for each iteration of the loop. This is built-in behavior, enshrined in the ECMAScript specification. Each closure gets its own copy of i. Performance? Excellent — the engine optimizes this at the compilation level.

However, there is a nuance: if you use let inside the loop but outside the for block, the behavior may differ. Always keep let inside the loop header.



Solution #3: bind — A Functional Approach

The bind method allows you to fix arguments:

for (var i = 0; i < 5; i++) {    setTimeout(function(j) {        console.log(j);    }.bind(null, i), 100);}

Here, bind creates a new function with a preset argument. This works, but creates an additional function for each iteration. In terms of performance, bind is slightly slower than IIFE due to internal checks.



Solution #4: forEach and Other Array Methods

Sometimes the best way to avoid the problem is to not use for:

[0, 1, 2, 3, 4].forEach(function(i) {    setTimeout(function() {        console.log(i);    }, 100);});

forEach creates a new scope for each iteration via the callback. This not only solves

Blogs

Book Recommendations