Friday, April 19, 2013

Callback functions in loops in Javascript

In other to implement the Reddit twitter bot for r/Java (@redditJava) and r/programming (@redditprogrammn), I had to get the lists of entries from Reddit, loop through it and call a function to tweet each item. On successfully tweeting, a call back function is called that stores the id of the item that was tweeted. This list of saved Id is checked every time, before tweeting, in other to prevent double tweeting.

A simple way to get this done is to get the items to be tweeted in a form of an iterable (array or object in javascript), loop through the items and in the loop, put the logic that tweets the item and the call back function.

A pseudo code would look thus:
for (var n=0; n < thingstotweet.length; n++) {
      // tweet function
      Tweet(thingstotweet[n], function(e,r) {
         // the call back function
          //if no error, then save the id of the tweet
          if (!e) {
              Db.save(thingstotweet[n]);
          }
      })
   }

In Javascript, this is a faulty implementation. If you implement such a solution for having callbacks inside a loop you would soon find out that the things gets out of sync. For instance in the tweeting example, one tweet would be sent while a different one would be saved in its place. And the reason is simple: It takes time before the function sending the tweet returns and the call back is called. And since functions call like this in Javascript are non blocking, in the time it takes before the calling function returns and the callback called, the value of n would have changed leading to saving the wrong item. 

For example.
1. Start the loop when n is 0;
2. Tweet the item in index 0 in the thingstobetweet list
3. The tweet function is sending the tweet but it takes a couple of seconds.
4. Value of n increased to 1. Still tweet has not been sent, so call back Is not to be called yet
5. Value of n increased to 2. Still tweet has not been sent, so call back is not to be called yet
6. Value of n increased to 3. Finally the tweet was sent successfully, time to call the callback
7. The call back is called. But n is now 3. So thingstobetweet[3] is saved for a tweet at thingstobetweet[0]

How then do you deal with this? What would be the correct way to implement a task that needs a callback function inside a loop?

There are two ways I normally implement a solution for this specific scenario:

Use Closures

In a previous post I briefly explained closures in Javascript.  Dealing with callbacks in loop is a situation where closures can be put to use. The solution is to create a closure for the tweet function. A pseudo implementation would be thus:
  for (var n=0; n < thingstotweet.length; n++) {
      (function(clsn){
          // tweet function
          Tweet(thingstotweet[clsn], function(e,r) {
              // call back function
             //if no error, then save the id of the tweet
              if (!e) {
                  Db.save(thingstotweet[clsn]);
              }
          });
               
      })(n)
 }

You can check the post on closures in Javascript for more information and why the pseudo code above works.

Use Recursive Function

The other solution is to implement a recursive function.  An implementation would be thus:
 var tweetRecursive = function (n) {
      if (n < thingstotweet.length) {
          // tweet function
           Tweet(thingstotweet[clsn], function(e,r) {
              // the call back function
              //if no error, then save the id of the tweet
              if (!e) {
                 Db.save(thingstotweet[clsn]);
                 tweetRecursive(n + 1);
               }
           });        
       }
  }


  //start the recursive function
  tweetRecursive(0);


The logic to send the tweet is defined as a recursive function.  This works because the function to tweet the next item (the recursive function) is also called in the callback function. This would ensure that there is no out of sync situation and the correct tweet would be saved for the one that was sent.

The only repercussion in this implementation is that it breaks the asynchronous nature as the tweets would be sent in a procedural manner, i.e. one after the other instead of asynchronous fashion.

1 comment:

Chris Houng said...

Thanks for the post.
It helps me a lot