Dissecting the behavior of a ‘for’ loop created with ‘setTimeout’

Today, we will dive deep into a problem that is frequently presented in JavaScript interviews. Since my interaction with this problem, I've struggled quite a bit to understand what's going on behind the scenes. I looked for solutions online, but unfortunately, there wasn't a single solution that satisfied all my questions.

So, I started exploring the questions and found the answers that I've presented in this article. Hopefully, this discussion will clear up any confusion you may have on this topic as well. Let's get started.

Variables created with ‘var’ act weird

for (var i = 1; i < 5; i++) {
  setTimeout(() => {
    console.log(i); // 5 (4 times)
  }, 1000);
}
console.log(i); // 5

You might have encountered this problem already, where the above for loop logs '5' four times in the console instead of logging '1' through '4'. The key factor contributing to this behavior is that variables created with var are function-scoped.

Behind the scenes

In the above code, in each iteration of the loop, a block is created with a variable i, and a setTimeout function gets registered in the web API environment. The setTimeout function schedules a callback function that references the variable i within its scope to execute after a one-second delay.

Important to note that the variable i, being created with var, doesn't respect the block and becomes available in the global scope, making it accessible both inside and outside of the for loop.

Dynamics

In the first iteration of the loop, a variable i is created with the value of '1'. In the second iteration, another variable i is created with the value of '2'. Since both variables exist in the same scope, the value of the previous variable i gets modified to '2'.

Likewise, in the last iteration of the loop, another variable i is created with the value of '5', which modifies the value of the previous variable i to '5'. At this point, there is a single variable i in the scope with the value of '5'.

Notice that, even though the loop terminates when the value of i is '4', we still get '5' as the final value. This is because the value of i is already modified to '5' before the JavaScript engine reaches the loop termination logic.

Finally, as soon as the synchronous code is executed, the callback functions start executing. Each callback function executes, finds the value of i as '5', and logs it to the console. That's how the above for loop logs '5' four times instead of logging '1' through '4'.

Variables created with ‘let’ fix it

One way to fix this issue is to change the variable declaration to let.

for (let i = 1; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
console.log(i); // ReferenceError: i is not defined

Notice that, as soon as we've changed the variable declaration from var to let, the variable i is no longer accessible outside of the for loop. This is because, unlike var, variables created with let are block-scoped and only accessible within the block they are created.

Behind the scenes

Identical to the solution using var, in each iteration of the loop, a block is created with a variable i, and a setTimeout function gets registered in the web API environment. The setTimeout function schedules a callback function that references the variable i within its scope to execute after a one-second delay.

However, the difference is made by the usage of let in this solution. Since let is block-scoped, a variable created with let in the current scope cannot access or modify another variable created with let from the scope created in the previous iteration. This results in each scope having a unique variable value for i.

Finally, when the callback functions execute, each function refers to the variable i from its respective scope and logs the value to the console. This effectively fixes the weird behavior introduced by using var.

That's it. Hopefully, you now know the reasons behind this problem and how to fix it. Along with this learning, you now also have another reason not to use var anymore to avoid unexpected behaviors like this.

By the way, if you are interested in a deeper dive into the concepts of scope and how synchronous and asynchronous code is executed in JavaScript, I have articles on Exploring the scope and scope chain in JavaScript and Exploring the inner workings of a JavaScript runtime that provide further insights.

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!