Encapsulate Your Scope When Looping With Asynchronous Functions

When using for loops in JavaScript, sometimes you can encounter some unexpected behaviour.

First, a simple for loop– No tricks:

for (var i = 0; i < 5; i++) {
  console.log(i);
}

As anyone would expect, this will print out 0, 1, 2, 3, 4 to the console.

Now, what if we are operating asynchronously? Next, let’s essentially do the same thing, except with a 1 second delay between console.log calls:

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

However, we may be dismayed to discover that what happens (JSFiddle) is, that, although we got the delay we wanted, every console.log call is printing “5”! So, what happened?

The answer is simply that by the time the callback function runs, i is already 5. That’s why the loop terminated, after all.

Even if we change the code to have a delay of 0, it will still print “5” 5 times:

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

The reason for this is the JavaScript EventLoop pipeline. All the synchronous code is executed before any of the asynchronous code.

The solution (JSFiddle) is to encapsulate the scope of the callback method.

for (var i = 0; i < 5; i++) {
  (function(x) {
    setTimeout(function() { console.log(x); }, x*1000);
  }(i));
}

The key difference here, is that we are creating a new function scope which we pass i to synchronously. That way, when the function actually runs later, x will be what i was when we originally queued it up.

These were trivial examples using setTimeout for demonstration purposes, but some common real-world examples include:

  • Creating callback functions
  • Attaching event handlers
  • Performing AJAX queries
  • Database operations

When I conduct interviews, if the applicant has listed JavaScript on their resume, I’ll ask them this question. It’s shocking how few people can guess what will happen. I’ve also seen developers get bitten by this when trying to insert rows from an array into a database using the async insert call, instead they ended up inserting the last element of the array as many times are there were elements in the array.

One thought on “Encapsulate Your Scope When Looping With Asynchronous Functions

Comments are closed.