Mastering Asynchronous Javascript: Part 1 – Callback Hell

The Asynchronous Nature of Javascript

Asynchronous programming plays a very important part in JavaScript development. Asynchronicity means that events can come in any order rather than the code just following a predictable, linear path. This can be a confusing for the budding developer, as many people expect a result as soon as they ask for it. It means that it’s important to know what tools are at your disposal for dealing with asynchronous events.

There are many events in JavaScript that could be considered asynchronous:

  • Database queries – we don’t know when the result will come
  • HTTP calls – we don’t know when the resource will finish loading
  • File access – we don’t know when we have finished reading the file
  • User generated events in the browser – a user could click at anytime
  • Timers – they fire sometime in the future

Basically anytime something happens sometime in the future, as opposed to immediately, we consider this to be an asynchronous event.

The Event Loop

Now I have impressed upon you the importance of asynchronous programming in JavaScript I’m going to throw you a curve ball; JavaScript is actually a synchronous language. If you’re confused, don’t worry; all will be explained.

We’re going to take timers as our example, as it allows me to show you something that will illustrate the point really well.

So let’s create a timer:

function createTimer() {
    setTimeout(function () {
        console.log('The timer fired');
    }, 1);
}
createTimer();

This code creates a function that registers a timer. The timer callback function prints ‘The timer fired’ to the console after 1 millisecond. Go ahead and paste that into the console. You will see ‘The timer fired’ printed almost instantly, since 1 millisecond is too quick for us to perceive. Now try pasting the following code into the console:

function createTimer() {
    setTimeout(function () {
        console.log('The timer fired');
    }, 1);
    for (var i = 0; i < 10000000; i++) {
        var d = new Date();
    }
}
createTimer();

What happens now? You may be surprised to see a small, but noticeable delay between entering the code and the console showing ‘The timer fired’. It takes more than one millisecond. This small delay is down to the fact JavaScript is actually synchronous. It has a single thread of execution, which when blocked can delay the handlers associated with asynchronous events.

The following happens in the example above:

  1. A timer is created
  2. We enter into a loop creating a new date object 10 million times. This takes time.
  3. The callback function registered to the timer cannot fire until our date creation loop has finished executing. This is because JavaScript only has a single thread of execution, and it is stuck within the date loop till it exits.

For this reason, the delay argument passed to setTimeout is actually a minimum time to wait until we call the callback, since there is no guarantee it will fire at exactly that time if the thread gets blocked by something else. To understand why this is, you need to understand how JavaScript’s event loop works.

At runtime, JavaScript stores a queue of messages. Each message is created when some asynchronous action occurs. This could be an HTTP request returning, a database result coming in, or in this case a timer firing. Each message in the queue has a function associated with it. The event loop iterates the queue on a regular basis, executing the function associated with each message and then removing that message from the queue.

In the case of our ‘late’ timer, the message will be added to the queue when the timer fires, but JavaScript’s single thread of execution has to wait for the loop in which all the dates are created to exit before it can move to executing the functions associated with the event queue’s messages, in this case the timer.

Callbacks

You may be thinking that all that was a big detour, after all you came here to learn about callbacks. It is however important to grasp how asynchronous events work in JavaScript before learning how to handle them. Ignorance is not bliss in programming! Now let’s move on to callbacks.

Callbacks are the simplest way to handle asynchronous events in JavaScript. A callback is simply a function designed to handle an event. When the event occurs, the callback will be called. To me more precise, when a message is added to the messages queue, the callback will be associated with it and executed when the event loop gets around to iterating the queue. Let’s look at a simple example.

// Create callback function
function myCallback () {
    console.log('Clicked');
}

// Assign callback to be called each time the document is clicked
document.addEventListener('click', myCallback);

This is really simple. Define a function. Assign it as a callback. Done! Before you think you now know all you need to in order to handle asynchronicity in JavaScript, I have some bad news. There are many problems with callbacks.

The Problem With Callbacks

The best way to show the problem with callbacks is using some code.

var MongoClient = require('mongodb').MongoClient
var url = 'mongodb://localhost:27017/myproject';
function output (user, orders) {
    // output data in pretty format
}

function logError (error) {
    // Log the error (could be console, a service, a file etc)
}

// Connect to mongo server
MongoClient.connect(url, (err, db) => {
    const userId = process.argv[2];
    db.collection('access_log').insert({ userId, date: new Date() }, (err, result) => {
        if (err) {
            logError(err);
        } else {
            db.collection('users').find({ userId }).toArray((err, users) => {
                if (err) {
                    logError(err);
                } else {
                    db.collection('orders').find({ userId }).toArray((err, orders) => {
                        if (err) {
                            logError(err);
                        } else {
                            output(users[0], orders);
                        }
                    });
                }
            });
        }
    });
});

The code connects to mongo, logs the access a log table, finds the user record for the required user, finds all orders for the given user, then finally passes these to a function (output) which will format and output this data. Any errors are handled using the logError function.

There’s two very obvious things wrong with this code: poor readability and repetition.

The poor readability comes from the deep nesting of each callback. The code keeps drifting to the right, and it gets hard to see where each block ends. These things would make the code hard to read and maintain, and this problem would be exasperated if we added even more code within each block. The reason the blocks have to be nested is because each action is dependant on the previous one finishing without error, and so each function call has to be nested within the callback of the previous action’s function call.

The repetition is the part where we check for errors:

If (err) {

logError(err);

} else {

//….

}

At this point some readers may be saying to themselves, “Ahh, he doesn’t know about exception handling! Just wrap all your code in a try block and catch errors in a catch block”. This won’t work, and it’s a big issue with callbacks. Why won’t it work? It won’t work because the callbacks will be defined within the try block, but they won’t get executed from within the try block, because they get run at some later time (when the database result returns). This means by the time there’s an error to catch the execution context will no longer be within the try block, instead it would be somewhere within the mongodb module where the callback is called. It’s thrown and we can’t catch it (except maybe with a global error handler callback).

In summary, callbacks:

  • Are very simple to understand
  • Built into the language since they’re just functions
  • Can easily lead to code that is hard to read and maintain
  • Make error handling a pain in the ass

Next Time

There are other ways to handle asynchronicity in JavaScript. Next post I’ll teach you all about the first of these, Promises.

The full series:

  1. Callbacks
  2. Promises
  3. Generators
  4. Async / await
  5. RxJS

Leave a Reply

Your email address will not be published. Required fields are marked *