Async await demystified

In the beginning...

When Javascript was first created it was intended for simple tasks within the context of a single webpage. Most scripts were one or two lines embedded in the attributes of an HTML element. A script was triggered in response to an event such as clicking on a link and that was it.

This worked great for scripts that had everything they needed to complete immediately. But what if you wanted to do something on a delay, such as updating a clock on the website? Javascript was single-threaded and ran in the UI thread of the browser so if you tried to sleep or spin until the right time you would end up locking up the whole browser.

Javascript's solution to this was callbacks:

setTimeout(5000, function () {
  alert("It's 5 seconds later!");
});

Because Javascript allowed you to easily define functions in-line this solution was simple and worked well as long as the programs people were writing in Javascript were realtively simple.

Callback hell

As Javascript gained ever more capability the limitations of callbacks began to appear. The main issue with callbacks is that the logical sequence of your program becomes very convoluted. For example:

console.log("This comes first");
setTimeout(1000, function () {
  console.log("This comes fourth");
});
setTimeout(100, function () {
  console.log("This comes third");
  setTimeout(1000, function () {
    console.log("This comes fifth");
  });
});
console.log("This comes second");

You can also see that as you start nesting callbacks, the code becomes more and more indented, experiencing a sort of rightward drift. There had to be a better way.

Promises, promises

The Promise API was developed by library authors to try to tame the callback hell. It introduced an object called a Promise which represents a computation that will complete at some point in the future.

// This function converts the setTimeout() API into a Promise API
function timeoutPromise(delay: number, value: string): Promise<string> {
  return new Promise((resolve) => setTimeout(delay, () => resolve(value)));
}

As you can see, we now have a function that returns a Promise rather than taking a callback as an argument. Inside that function we pass a callback to the existing setTimeout API that "resolves" the promise.

What can we do with that Promise object? Well, Promise has a method then() which allows us to give it a callback to be called when the Promise resolves.

let promise = timeoutPromise(100, "a");
promise.then((value) => {
  console.log("The timeout finished! Here's your value:", value);
});

But now we're back where we started, just passing callbacks to promises instead of the original functions! What's the point?

Well Promise has another trick up its sleeve. The then() method itself returns a Promise so you can chain its calls:

let promise = timeoutPromise(100, "a");
promise
    .then((value) => {
        console.log("The timeout finished! Here's your value:", value);
        return "b";
    })
    .then((value) => {
        console.log("This time the value is 'b':", value);
    });

What if we want to do something asynchronous inside then()? Won't we still need to nest promises?

Well, here's the final trick. If you return a Promise from the then() callback, then the chained promise will wait for the promise you returned. That lets you turn that chain of nested callbacks into a linear chain of promises instead.

// This function converts the setTimeout() API into a Promise API
function timeoutPromise(delay: number, value: string): Promise<string> {
  return new Promise((resolve) => setTimeout(delay, () => resolve(value)));
}

console.log("This comes first");
timeoutPromise(100, "a").then((value) => {
  console.log("This comes second, value is 'a'", value);
  return timeoutPromise(100, "b");
}).then((value) => {
  console.log("This comes third, value is 'b'", value);
  return timeoutPromise(100);
}).then((value) => {
  console.log("This comes forth, value is 'c'", value);
});

As you can see, this API solves the rightward drift problem. It makes the flow of control more clear in that things now generally happen from top to bottom in the code, instead of having to look for ever more nested callbacks. However, there's still an awful lot of callbacks being created and asynchronous code using this API is still much more convoluted than equivalent synchronous code would be. Maybe there's a better way...

A little sugar please

Finally we come to async await. This feature was added to the Javascript language after the Promise API was standarized and is really just syntax sugar for that API. Everything you do with async await can be done with the underlying Promise API, async await just makes the code easier to read, write and understand.

First let's look at async:

async function foo(): Promise<string> {
  return "bar";
}

async does exactly two things. First, it wraps the return type of your function in a Promise. Second it allows you to use the await keyword. That's it. So the above function is exactly the same as:

function foo(): Promise<string> {
  return new Promise((resolve) => resolve("bar"));
}

The real magic is in the await keyword. await is used on a Promise object (for example, the one returned by an async function, but any Promise will do). When you use the await keyword it takes everything in the function that comes after it and essentially turns it into a callback that is passed to the then() method of the Promise. In other words it lets you write code sprinkled with awaits that looks just like normal synchronous code, but automatically transforms it into code that uses the Promise API to wait for completion at each step. So our promise example above would become:

// This function converts the setTimeout() API into a Promise API
function timeoutPromise(delay: number, value: string): Promise<string> {
  return new Promise((resolve) => setTimeout(delay, () => resolve(value)));
}

async function main(): Promise<void> {
  console.log("This comes first");
  let value = await timeoutPromise(100, "a");
  console.log("This comes second, value is 'a'", value);
  let value = await timeoutPromise(100, "b");
  console.log("This comes third, value is 'b'", value);
  let value = await timeoutPromise(100);
  console.log("This comes forth, value is 'c'", value);
}

main();

You'll note we had to wrap things in an async function in order to use the await keyword. But the code is now much shorter and clearer than it was before.

And that's all there is to it!