Thinking Asynchronously in CoffeeScript/JavaScript: Loops and Callbacks

Awhile back, I wrote about my new experience in learning Node.js: A Node.js Experiment: Thinking Asynchronously, Using Recursion to Calculate the Total File Size in a Directory.

Consider this snippet of code:

var names = ['JP', 'Chris', 'Leslie'];
for (var i = 0; i < names.length; ++i){
  var name = names[i];
  setTimeout(function(){
    alert(name);              
  },10);
}

Equivalent CoffeeScript:

names = ['JP', 'Chris', 'Leslie']
for name in names
  setTimeout(->
    alert(name)
  ,10)

Click here%0A%20%20%2C10)) to run it. If you guessed that the loop would alert "Leslie" three times, then you'd be correct.

The problem is, that before the callback executes, the loop has completed. Thus callback always has the last value.

How do you solve this problem? You wrap the callback in a closure that executes immediately.

JavaScript:

var names = ['JP', 'Chris', 'Leslie'];
for (var i = 0; i < names.length; ++i){
  var name = names[i];
  (function(name){
    setTimeout(function(){
      alert(name);              
    },10);
  })(name);
}

CoffeeScript:

names = ['JP', 'Chris', 'Leslie']
for name in names
  do (name) ->
    setTimeout(->
      alert(name)
    ,10)

Click here%20-%3E%0A%20%20%20%20setTimeout(-%3E%0A%20%20%20%20%20%20alert(name)%0A%20%20%20%20%2C10)) to run it.

These solutions execute the block of code in a parallel manner. Using the alert's are not a good indication in showing this behavior. However, if you were opening files, all of them would be opened approximately (not exactly) at the same time.

What if you wanted to perform the action in the callback in a serial manner?

Using the previous simple example, it'd look like this:

JavaScript:

var names = ['JP', 'Chris', 'Leslie'];
loop = function(i){
    setTimeout(function(){
      alert(names[i]);
      if (i < names.length - 1)
        loop(i + 1);       
    },10);
}
loop(0);

CoffeeScript:

names = ['JP', 'Chris', 'Leslie'];
doloop = (i) ->
  setTimeout(->
    alert(names[i])
    if i < names.length - 1
      doloop(i + 1)       
  ,10);
doloop(0)

Run it.%20-%3E%0A%20%20setTimeout(-%3E%0A%20%20%20%20alert(names%5Bi%5D)%0A%20%20%20%20if%20i%20%3C%20names.length%20-%201%0A%20%20%20%20%20%20doloop(i%20%2B%201)%20%20%20%20%20%20%20%0A%20%20%2C10)%3B%0Adoloop(0))

If you were doing file processing in the loop, it would be executed sequentially.

Hopefully this helps you to better understand asynchronous design of algorithms in JavaScript.

Update: I forgot about the forEach function that exists in Node.js and most modern browsers. This function pretty much solves the problem.

Here's the JavaScript code:

var names = ['JP', 'Chris', 'Leslie'];
names.forEach(function(name){
  setTimeout(function(){
    alert(name);              
  },10);
});

Much cleaner. Thanks to smog_alado [Reddit] for the reminder.

If you made it this far, you should follow me on Twitter.

-JP

comments powered by Disqus