The Linux Page

JavaScript async & await, promises, and callbacks

JavaScript Asynchronous System, how callbacks work

This pictures shows three functions (F1, F2, and F4) and their callbacks (C3, C5, C6. C7, C8, and C9.) The graph shows the order of execution from left to right. As we can see, JavaScript functions and callbacks get executed one after the other. However, the order can seem quite random. This is often referenced as Callback Hell. Promises were going to fix that, but now we have async/await which is even better! Although the execute remains the same (as shown here,) writing the code becomes much more straight forward. For example, changing the graph here with await, it would clearly look like three functions:

await F1
await C3
await C5
await C8

await F2
await C7
await C9

await F4
await C6

Also JavaScript does not have all the multi-threading issues (variables that can be modified by one function while running another), you can still have interesting side effects if you're not careful. The following tries to dig further in the problem.

What is Asynchronous?

More and more functions in JavaScript are asynchronous. This is very practical as modern implementations of JavaScript can make use of multiple threads which are completely hidden to the JavaScript developer. This allows for many functions to run in parallel even though JavaScript itself is still itself a one thread process (there are talks about making threads available into Node.js. Browsers already have a form of "backend process" capability, but it's fairly underused.)

The concept dates from way back. In other words, the concept of having asynchronous execution is nothing new. Already on my Apple //c I could run software this way. Not only that, but these good old Apple computers (and first Macs) did not have multiple CPUs and even less multiple cores. Everything had to be run by a single CPU.

So a simple concept born way back when we had limited hardware was to have a form of array of actions. You would run a small part of an action, return, run a small part of the next action, return, etc. until all the work was done. Eventually, some actions never ended (permanent animation...)

JavaScript offers the same capability with callbacks. It stacks callbacks in an array and whenever the corresponding event occurs, the callback may get removed from that array (i.e. setTimeout(), for example, generates a callback once only, then that callback gets removed from the array of actions.)

Callback Hell!

At first, JavaScript had a fairly few number of functions that would take a callbacks and you generally would have only one level.

With each new iteration, though, new functions would be added, each of which wants to run in the background, meaning that to get the results, you have to specify a callback when you call the function. Then you own process returns and later on, once the function you called finishes, you finally get your callback executed.

If you use that with many encapsulated functions, you end up with something like this:

a.func({
  b.func({
    c.func({
      d.func({
          ...etc...
      });
    });
  });
});

One or two of those, you wouldn't die trying to follow the flow.

Now that we have Node.js and have access to the file system, databases, etc. all sorts of new functions were added and all have a callback. This becomes what people call the Callback hell.

A first solution is to create a separate function and references them in the callback.

function a()
{
  a.func(b)
}

function b()
{
  b.func(c)
}

function c()
{
  d.func(...)
}

This makes it easier to read the code, but it's really difficult to follow the actual flow. Because of that, it's often called spaghetti code. It is also very difficult to manage errors which won't automatically propagate with such a design.

The JavaScript Promise

To help, a library was created called Promise. This was a promise by JavaScript to help you with handling your spaghetti code in such a way that it would look like a list of functions, but tightly close together and with a central error manager.

To create a promise, you use the new operator. You get your callback called with a resolved and rejected set of functions. One of these must be called to end the promise properly.

let p = new Promise((resolve, reject) => {
    // your function does work
    ...
    // if an error occurs
    if(err) {
       // notice that we do not throw, we instead call reject
       return reject(new Error('Oops'))
    }

    // if the function succeeds
    return resolve(result)
  });

Once a promise was created and the first function implemented, you can implement the following functions in the .then() promise function.

p.then((result) =>{
    // another function
  })

The then is expected to return another promise which again has a resolve and a reject pair of functions.

You can have as many .then() as you want (i.e. chain them.)

To make error handling easier, the reject() function is called and the promise calls its .catch() handler (Another callback!)

p.then((result) =>{ ... })
  .then((result) =>{ ... })
  .then((result) =>{ ... })
  .then((result) => { ... })
  .catch((err) => { ...error handle... })

The final catch gets the error passed to any reject() function. Easy enough, but in reality, somewhat cumbersome to use on a daily basis.

Integration to the Language: async/await

In order to ease the handling of promises, especially because of the rather large number of those, a new syntactical feature was introduce to the language. The async and await.

The async appears in function declarations.

When calling an async function, you must call it using an await (and unfortunately the execution environment doesn't currently tell you if you forget to do so.)

There is a great advantage to this syntax, it's very light and makes your spaghetti code look like a fairly normal stream of statements and it still gives you access to the full power of asynchronous coding.

The basics go like this:

async function a()
{
  await b();
}

Up to here, nothing too extraordinary. b() is either an asynchronous function itself or it returns a promise. So we could have:

async function b()
{
  await c();
}

or:

function b()
{
  return Promise((resolve, reject) => {
      ...your code here...
    });
}

Certain system functions do not have an async version and require you to create a promise. We can that wrapping. For example, the setTimeout() function can be wrapped in this way:

async function sleep(ms)
{
  return new Promise((resolve, reject) => {
      setTimeout({
          resolve('tick');
        }, ms);
    });
}

(As a side note: notice how we already have two callbacks defined in one another...)

In this example, there is no call to reject() as we have no error conditions.

Now we can call the sleep() function with await and the code at that point will continue only after at least ms milliseconds have past.

Note that the await instruction can only be used in front of a function marked as an async function and also within an async function. This means you can't call the sleep() function from the global scope. You must create a function to be able to do that:

async main()
{
  await sleep(10000);
}

main();

Anyone can call an async function as is, however.

Note that since sleep() will return a Promise object, it is possible to call .then() on it. So in the global scope you could still do this:

sleep(10000).then({}).catch({})

which looks a bit funny but works as you would otherwise expect.

Exception (a.k.a. Rejection) Handling with async/await

With the async and await instructions, exception handling now becomes very clean since the implementation is allowed to use the normal try/catch implementation from the language.

However, the await keyword means that a reject() function gets called on an error and at some point you must handle the rejection one way or the other. What this means is that all aync function must use await on all async functions it calls and at least the top level async function must have a try/catch error handler to make sure that the error gets handled.

So in our example above, we had a, b, c, d... we could rewrite that code in something like this:

async function main()
{
  try
  {
    await a();
    await b();
    await c();
    await d();
  }
  catch(err)
  {
    console.log(err);
  }
}

The functions a(), b(), c(), and d() do not need to include a try/catch (unless they actually need to act on some errors.)

Unhandled Promise Rejection Error

Note that if an await or an async is missing, you are likely to get runtime errors. However, if an await is missing and the async function being called throws an error, you will get the following error:

(node:13514) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)

which may incite you to add many try/catch all over the place.

Look closer at your code, you likely have something like this:

async function a()
{
  try
  {
    await b()
  }
  catch(err)
  {
    console.log(err)
  }
}

async function b()
{
  c();
}

async function c()
{
  await d();
}

...

As we can see here, the function c() gets called without the await. Unfortunately, the JavaScript environment doesn't tell you about that mistake. As a result you get that error above telling you that the error(s) in c() or d() are not handled. The truth is that by adding await the errors will be propagated up the stack to the try/catch of function a().

I find that error pretty easy to make and I think many of us have been scratching their head for a while when they got it and did not notice the missing await.

Reason behing allow calls to async function without an await

A function marked with async returns a Promise, not matter what. If you return something else than a promise, the async wrapper will make sure to transform the function return value to a Promise anyway.

This means you can call multiple async function, collect all the results in an array and then call Promise.all(said_array). You will then be awaiting on all the promises in parallel. In means that you can start multiple background threads which all run in parallel instead of running one, wait, the next, wait, the next... In a server, this is not a bad thing since while one user is waiting another can still be working. In a desktop or browser environment, however, it can be much more beneficial to run multiple threads simultaneously rather than in a serial manner.

So, say you can run a(), b(). and c() in parallel, you won't use await on them because if you do, you'd block there... Instead you can do something like:

let p = [a(), b(), c()]
Promise.all(p).then((result) => { ...further code... })

You also get the results in an array, in the same order as the functions you called. So result = [A, B, C] where A represents the result of a(), B the result of b(), and C the result of c().

The await syntax can still be used if you prefer:

let pa = a()
let pb = b()
let pc = c()

let ra = await pa
let rb = await pb
let rc = await pc

Here I saved promises returned by a() in pa, but get promises pb and pc before awaiting on pa. That means b() and c() can run in parallel and end before or after pa is done.

The result is the same and it certainly looks more modern since we use the async/await syntax all the way and just like with promises, you can await on pa and do some other work before awaiting on pb.

let ra = await pa
...do some other work here...
let rb = await pb

If you want to be able to check whether one of ra, rb, or rc is done, use the Promise .race() instead.

Promise.race([pa, pb, pc]).then((result) => { ... })

The race() function calls the then() callback once per promises in the order in which they get resolved or rejected.