Good Things Come to Those Who Await

Tom Wey

ES8, otherwise known as ES2017, introduced async/await as an alternative to using Promises directly for writing and interacting with asynchronous code.

Using async/await

The async function (or its counterpart, the async function keyword) and the await expression can be combined to produce asynchronous code that reads similarly to synchronous code.

Specifying that a function is async has two main effects:

  1. It causes the return value of the async function to be a Promise resolving with the return value of the original, passed function.

  2. It allows us to use the await expression within the function.

Examples

It’s probably easiest to illustrate with some examples. The following is the most basic case:

const plainFunction = () => "bar";

const asyncFunction = async () =>
  plainFunction()

plainFunction() // => "bar"
asyncFunction() // => a Promise which resolves with "bar"

Following on from this, if we have a function which itself is asynchronous (i.e. returns a Promise), in our async function we can call it, pause execution and wait for it to complete before continuing, using await:

const fetchAndTransformPost = async () => {
  const response = await fetch("https://api.example.com/v1/posts/1");
  const post = await response.json();
  const transformedPost = transformPost(post);
  return transformedPost;
};

fetchAndTransformPost() // => a Promise which resolves with `transformedPost`

Let’s break this down:

  1. The fetch call returns a promise:

    const response = await fetch("https://api.example.com/v1/posts/1");
    

    The presence of await causes us to suspend execution of this code path until the promise has resolved. Once resolved the return value is the value the promise resolves with, in this case a Response object. So this is the value we assign to response.

  2. Next we decode the JSON body of the response:

      const json = await response.json();
    

    Again this call is asynchronous and returns a promise. As above we suspend execution of this code until the work is complete. The return value, and assignment, is the value of the resolved promise, in this case a JavaScript data structure representing the decoded JSON.

  3. The final part performs some transformation on the decoded JSON and returns the transformed data:

      const transformedJson = transformJson(json);
      return transformedJson;
    

    The return value of fetchAndTransformJson is a Promise which resolves with transformedJson.

In here we have two function calls which return promises - fetch() and response.json(). Without async/await we’d have used then to create a promise chain. What I like about this is how much it looks like regular old sychronous JavaScript code (ignoring the fact that we’re probably mixing too many concerns in this one function!)

More complex scenarios

So far our examples have been fairly straightforward. I think where async/await really shines is in more complex scenarios where we have synchronous code interspersed with our async code.

In this example we fetch a post from an API endpoint and, if its status is “published”, fetch its comments from another API endpoint and merge them in:

This is how it looks written with async/await:

const withAsyncAwait = async () => {
  const response = await fetch(postEndpoint);
  const post = await response.json();

  if (post.status === "published") {
    const commentsResponse = await fetch(commentsEndpoint);
    const comments = await commentsResponse.json();

    return { ...post, comments };
  } else {
    return post;
  }
};

This is the equivalent written using the Promise API directly:

const withPromises = () =>
  fetch(postEndpoint)
    .then(response => response.json())
    .then(post => {
      if (post.status === "published") {
        return fetch(commentsEndpoint)
          .then(commentsResponse => commentsResponse.json())
          .then(comments => ({ ...post, comments }));
      } else {
        return post;
      }
    });

I find the async/await version more readable and easy to follow. I like how flat it is, and aside from the additions of async and await it reads just like synchronous code.

Concurrent async operations

Sometimes it’s necessary to trigger multiple asynchronous operations concurrently, and wait for them all to complete before returning. Using await alone doesn’t cut it, since it always waits for the operation to complete before continuing. Let’s say we have two asynchronous operations, which we want to wait for and then return in a single comma separated string:

const multi = async () => {
  const result1 = await generateResult1();
  const result2 = await generateResult2();
  return [result1, result2].join(",");
}

This will have the return value we’re looking for, but generateResult2 won’t run until generateResult1 has finished. Since there’s no dependency, there’s no need to wait. Instead, we need to lean on Promise.all and await the result of that before continuing:

const multi = async () => {
  const results = await Promise.all([generateResult1(), generateResult2()]);
  return results.join(",");
}

Top level handling

We can’t always escape the fact that, behind the nice syntax provided by async/await, we’re dealing with Promises.

At the entry point to our top level async code we probably want to ensure that we’re handling any errors which may occur in our downstream async code with a catch handler:

asyncFunction()
  .catch(error => console.log("Something went wrong: ", error));

Alternatively within our async functions we could wrap any await calls with a try/catch block to handle downstream failures and ensure we don’t return a rejected promise:

const asyncFunction = async () => {
  try {
    return await thisMayFail();
  } catch(e) {
    return DEFAULT;
  }
};

Likewise, because we can’t use await outside of an async function, if we care about the final return value of our async code, we have to use the Promise API directly and use then:

asyncFunction()
  .then(result => console.log("Something went right:", result))
  .catch(error => console.log("Something went wrong: ", error));

Wrapping Up

I’ve been using async/await liberally on a project and have been really happy with the improvements it’s brought to code readability, especially in more complex cases involving logic based on the result of async operations.

There’s reasonable browser support for async/await and it’s available in recent Node versions, but depending on your use case you may need to leverage a tool like Babel to make use of it.