Sunday, August 28, 2011

Bitten By Bugs - the Asynchronous Way

Leave a Comment
Can you tell why the following code produces this output?

Source:
function getHighScores () {
  var highScoresDb = openDatabase('highScores', '1.0', 'My High Score list', 25500);
  var highScores = [];
  highScoresDb.transaction(
    function ( tx ) {
      tx.executeSql("select * from hiscores order by score desc", [],
                    function ( tx, results ) {
                      for ( var i = 0; i < results.rows.length; i++ ) {
                        highScores.push( results.rows.item( i ) );
                      }
                      console.log( "Num scores accumulated: ", highScores.length - 1 );
                    });
    });
    return highScores;
  }
var highScores = getHighScores();
console.log( "Num high scores returned: ", highScores.length - 1 );


Output:

Given the title of this post it might not be very hard to spot what's going on - but if you got your mind dialed in on procedural programming, it's not always easy to catch bugs like these.

Dealing with multi-paradigmatic programming languages like Javascript offers the possibilities of altering between and combining different programming paradigms. It's not uncommon to write procedural programs in Javascript while still using it's object-oriented features as well.

Javascript, like Perl, supports callbacks and closures quite elegantly. This is very useful when writing event-driven systems such as asynchronous I/O. Basically it let's you call a function and instead of hanging around waiting for the function to complete, you rather give the function a reference to a callback that will be invoked whenever the results are ready.

So now it might be clearer what's going on in the example;
  • 'highScores' is initialized as an empty array.
  • Both the call to the database object's transaction-method and the transaction object's (tx) executeSql-method are asynchronous methods that takes callbacks to be executed.
  • The execution of getHighScores is finished before the results are back from the database, thus it will return the empty array, and the log will show -1 since we haven't received any high scores - yet.
Bugs like these can be hard to find, especially if the callback was executed just fast enough on your computer but slower on other computers. You'd then have the dreaded non reproducable bugs.
(Actually - on my MBP with SSD the database returns the results before console.log is called)

How to fix it?

The most obvious and natural solution is to refactor our program to be callback driven itself.
This can be achieved by rewriting the getHighScores method to take a reference to a function that will be called once the results are ready:

function getHighScores ( callback ) {
  var highScoresDb = openDatabase('highScores', '1.0', 'My High Score list', 25500);
  highScoresDb.transaction(
    function ( tx ) {
      tx.executeSql("select * from hiscores order by score desc", [],
                    function ( tx, results ) {
                      var highScores = [];
                      for ( var i = 0; i < results.rows.length; i++ ) {
                        highScores.push( results.rows.item( i ) );
                      }
                      callback( highScores );
                    });
    });
  }
getHighScores( function ( highScores ) {
  console.log( "Num high scores returned: ", highScores.length - 1 );
});
Read More...