Ajax requests in a loop and other scoping problems with JavaScript

In the current code I’m working on, I had to iterate over an array and execute an Ajax request for each of these elements.

Then, I had to do an action on that element once the Ajax request resolved.

Let me show you what it looked like at first.

The code

1
2
3
4
5
6
7
8
9
10
function Sample() {
var products = [{name: "first product"}, {name: "second product"}, {name: "third product"}];

for(var i = 0; i < products.length; i++) {
var product = products[i];
$.get("https://api.github.com").then(function(){
console.log(product.name);
});
}
}

NOTE: I’m targeting a GitHub API for demo purposes only. Nothing to do with the actual project.

So nothing is especially bad. Nothing is flagged in JSHint and I’m expecting to have the product names displayed sequentially.

Expected output

1
first product
second product
third product

Commit and deploy, right? Well, no.

Here’s what I got instead.

Actual output

1
third product
third product
third product

What the hell is going here?

Explanation of the issue

First, everything has to do with scope. var are scoped to the closest function definition or the global scope depending on where the code is being executed. In our example, var is scoped to a function above the for loop.

Then, we have the fact that variables can be declared multiple times and only the last value is taken into account. So every time we loop, we redefine product to be the current instance of products[i].

By the time an Http request comes back, product has already been (re-)defined 3 times and it only takes into account the last value.

Here’s a quick timeline:

  1. Start loop
  2. Declare product and initialize with products[0]
  3. Start http request 1.
  4. Declare product and initialize with products[1]
  5. Start http request 2.
  6. Declare product and initialize with products[2]
  7. Start http request 3.
  8. Resolve HTTP Request 1
  9. Resolve HTTP Request 2
  10. Resolve HTTP Request 3

HTTP requests are asynchronous slow operations and will only resolve after the local code is finished executing. The side-effect is that our product has been redefined by the time the first request comes back.

Ouch. We need to fix that.

Fixing the issue the old way

If you are coding for the browser in 2016, you want to use closures. Basically, passing the current value in a function that is executed when defined. That function will return the appropriate function to execute. That solves your scope issue.

1
2
3
4
5
6
7
8
9
10
11
12
function Sample() {
var products = [{name: "first product"}, {name: "second product"}, {name: "third product"}];

for(var i = 0; i < products.length; i++) {
var product = products[i];
$.get("https://api.github.com").then(function(product){
return function(){
console.log(product.name);
}
}(product));
}
}

Fixing the issue the new way

If you are using a transpiler like BabelJS, you might want to use ES6 with let variable instead.

Their scoping is different and way more sane than their var equivalent.

You can see on BabelJS and TypeScript that the actual problem was resolved in a similar way.

1
2
3
4
5
6
7
8
9
10
function Sample() {
var products = [{name: "first product"}, {name: "second product"}, {name: "third product"}];

for(var i = 0; i < products.length; i++) {
let product = products[i];
$.get("https://api.github.com").then(function(){
console.log(product.name);
});
}
}

Time for me to use a transpiler?

I don’t know if, for me, it’s the straw that will break the camel’s back. I’m really starting to consider using a transpiler that will make our code more readable and less buggy.

This is definitely going on my TODO list.

What about you guys? Have you encountered bugs that would not have happened with a transpiler? Leave a comment!