Asynchronous Programming

Learning Goals

  • Software design approaches and patterns, to identify reusable solutions to commonly occurring problems
  • Apply an appropriate software development approach according to the relevant paradigm (for example object oriented, event driven or procedural)

This topic looks at asynchronous function calls in JavaScript.

Asynchronous functions

What are asynchronous functions you ask? They are functions that are executed out of order, i.e. not immediately after they have been invoked.

You have in fact already come across at least one such function: setTimeout()

console.log(1);

setTimeout(function() {
    console.log(2);
}, 1000);

console.log(3);

// Prints: 1 3 2

As you have seen in the example above, the function passed to setTimeout is executed last. This function is called a callback function and is executed when the asynchronous part has completed.

A very common use-case of asynchronous functions and callbacks are HTTP requests, e.g. when fetching some data from another API:

var request = new XMLHttpRequest();
request.onreadystatechange = function() { 
    if (request.readyState === 4 && request.status === 200) {
        console.log("Response received:");
        console.log(request.response);
    }
};
request.open("GET", "http://localhost/", true);          
request.send(null);
console.log("Request sent");
console.log("Response not yet available!");

Here the callback is set manually: request.onreadystatechanged = function() {...}. The callback is only executed when the HTTP request has successfully made its way across the network and back.

This is a very common sight, as a lot of tasks on the web take time and we don’t want to stop our entire program to wait for the response to come back (and e.g. cause the browser to show the dreaded unresponsive script message), as in the following example:

var request = new XMLHttpRequest();
request.open("GET", "http://localhost/", false); // false for synchronous request
request.send(null); // ... waiting ...
console.log("Request sent");
console.log("Response available!");
console.log(request.response);

Promises

A common issue with callback functions arises when you need to enforce any kind of order between them. The result are nested callbacks, which further execute other asynchronous functions that have their own callbacks:

someAsyncFunction(function callback() {
    someOtherAsyncFunction(function anotherCallback() {
        yetAnotherAsyncFunction(function callbackHell() {
            // ...
        });
    });
});

To prevent this, you can extract each function and give it its own definition (instead of nesting them), but this means that the definitions of each of these functions is highly dependent on the definitions of all of the functions that eventually go inside it!

Instead, you can use promises. These give you a nice way to chain asynchronous calls, as well the possibility to pass errors along to the top-level function:

let promise = new Promise(function(resolve, reject) {
    someAsyncFunction(function callback(data, error) {
        if (error) { reject(error); }
        else { resolve(data); }
    });
});

promise.then(function success(data) {
    console.log("Asynchronous function was called and returned some data: " + data); 
}).catch(function (error) {
    console.log("Something went wrong");
});

Promises are essentially just a wrapper around callbacks. As you can see the promise constructor above actually makes use of a callback function, which in itself exposes the resolve and reject callbacks. This slight boilerplate is necessary to make asynchronous calls easier to chain. Note that most modern JS libraries actually return promises for any asynchronous calls, so you don’t have to wrap them in promises yourself!

The then function

The then function also returns a promise, which means that you can chain as many as you like – they will be executed in order and the result that gets passed in depends on what you returned from the previous then-block:

  1. the previous then-block doesn’t even have a return statement: the argument is just undefined
  2. the previous then-block returns a value: this value is passed as the argument to the next then-block
  3. the previous then-block returns another promise: the promise is executed first and the result is passed in as the argument to the next then-block

The last case is the most interesting, so here is an example:

const asyncPromise1 = new Promise(function(resolve, reject) {
    someAsyncFunction(function callback(data, error) {
        if (error) { reject(error); }
        else { resolve(data); }
    });
});

const asyncPromise2 = new Promise(function(resolve, reject) {
    someOtherAsyncFunction(function callback(data, error) {
        if (error) { reject(error); }
        else { resolve(data); }
    });
});

asyncPromise1.then(function success(data) { // First execute one...
    console.log("The first async function has completed");
    return asyncPromise2; // ...then the other!
}).then(function success(data) {
    console.log("The second async function has completed");
}).catch(function (error) {
    console.log("Something went wrong");
});

The catch

You may have noticed the .catch() statement at the end of the last example. This is where any rejected promises end up as well as any other errors that are thrown.

asyncPromise1.catch(function(error) {
    console.log(error);
    throw new Error("something went wrong");
    // alternatively: return Promise.reject(new Error("..."));
});

The above example also shows that you can re-throw caught errors. They will be handled by the next catch-block up the chain.

Note that throwing errors inside an asynchronous callback doesn’t work!

new Promise(function() {
    setTimeout(function() {
        throw new Error('This will not be caught!');
        // return Promise.reject('error'); This also won't work
    }, 1000);
}).catch(function(error) {
    console.log(error); // doesn't happen
});

Instead you will just get a warning about an uncaught exception or an unresolved promise.

In order to fix this you will need to wrap the asynchronous call in a promise and call reject:

function timeout(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error("error"));
        }, duration);
    });
}

timeout(1000).then(function() {
    // You can also throw in here!
}).catch(function(e) {
    console.log(e); // The Error above will get caught!
});

Nested promises

We have seen that executing multiple promises in order is as easy as returning promises in a then-block, but what if we need to combine data from these promises? We don’t want to revert back to nesting:

asyncFunctionReturningPromise1().then(function success(data1) {
    return asyncFunctionReturningPromise2().then(function success2(data2) {
        // Do something with data1 and data2 
    });
}).catch(function (error) {
    console.log("Something went wrong");
});

Fortunately, promises provide a number of useful functions, one of which is Promise.all:

Promise.all([
        asyncFunctionReturningPromise1(),
        asyncFunctionReturningPromise2()
    ]).then(function(result) {
    console.log(result[0]);
    console.log(result[1]);
});

This will cause the asynchronous operations in asyncFunctionReturningPromise1 and asyncFunctionReturningPromise2 to both start at the same time, and they will be running simultaneously. Once both individual promises have been fulfilled, the promise returned by Promise.all will be fulfilled, and the then callback will be run with the result of the two promises.

If for any reason you need the asynchronous operations to happen in a certain order, you can impose an order by chaining the promises as below:

const asyncpromise1 = asyncFunctionReturningPromise1();
const asyncpromise2 = asyncPromise1.then(function(result) {
    return asyncFunctionReturningPromise2();
});

Promise.all([asyncPromise1, asyncPromise2]).then(function(result) {
    // You get back an array with the result of both promises!
    console.log(result[0]);
    console.log(result[1]);
});

Don’t worry if you find the above a bit unintuitive or hard to read. Do make sure you understand why the above works though before moving on!

Async/Await

Built on top of promises, ES2017 introduced the keywords async and await. They are mostly syntax sugar built on top of promises, but make the previous example we looked at significantly easier to read:

const result1 = await asyncFunctionReturningPromise1();
const result2 = await asyncFunctionReturningPromise2();

// Do something with result1 & result2!

You can await on any promise, but functions which use await need to be marked as async, i.e.:

async function doWork() {
    const result1 = await asyncFunctionReturningPromise1();
    const result2 = await asyncFunctionReturningPromise2();
}

The catch (again)

With promises we were able to catch errors at the end of the chain on then-calls.

In order to do the same with await, we have to use a try/catch block:

try {
    const result1 = await asyncFunctionReturningPromise1();
    const result2 = await asyncFunctionReturningPromise2();
} catch (error) {
    // Uh oh, something went wrong!
}

Since we’re still dealing with promises under the hood, handling individual errors is also still possible:

const result1 = await asyncFunctionReturningPromise1().catch(function(error) {
    console.log(error);
    // rethrow if necessary: throw new Error("something went wrong");
});

Using async and await is a more modern syntax and is mush more widely used in recent code bases. Make sure you gain a greater understanding by reading further documentation.

The callback queue

What do you think the following code will do?

function callBackQueue() {
  console.log('Start');

  setTimeout(function cb() {
    console.log('Callback 1');
  }, 1);

  console.log('Middle');

  setTimeout(function cb1() {
    console.log('Callback 2');
  }, 0);

  console.log('End');
}

callBackQueue();

Try running it, and see if that matches your expectation. Try running it a few times more and see if anything changes!

The first bit to point out is that StartMiddle and End always get printed before either of the Callback messages, even though we specified a timeout of 0 for the second callback function.

This is because all callback functions are put on the callback queue when their asychronous function completes and functions in the callback queue are only executed when the current function has completed! This means that even though the first timeout function may have completed e.g. before console.log('End'), its callback only gets invoked after ‘End’ has been printed.

You may also have noticed that the order of the callbacks is not always 2 > 1 (the first one has a longer timeout, so should technically be printed later). The difference in time is deliberately small enough (1ms) that it is possible for the runtime to finish executing the first timeout before the second one. It is useful to keep this in mind when working with asynchronous JavaScript functions: they can complete in any order and their callbacks will only be executed after the current function has completed.