Asynchronous JavaScript: How to use async and await

Using JavaScript’s async and await functions makes for more readable and maintainable asynchronous code, but does have downsides

Let’s look at some new syntax introduced as a part of ES2017 to help organize code around promises. In many cases, this new syntax — namely the async and await keywords — will help you write more readable and maintainable asynchronous code, but it’s not without its drawbacks. We'll first look at how async and await can be used and then talk about some of the downstream implications of using it.

To start, we’ll lay out a simple example using promises and then refactor it to use async/await. In our example, we'll use axios, a promise-based HTTP library.

let getUserProfile = (userId) => {
  return axios.get(`/user/${userId}`)
    .then((response) => {
      return response.data;
    })
    .catch((error) => {
      // Do something smarter with `error` here
      throw new Error(`Could not fetch user profile.`);
    });
};
getUserProfile(1)
  .then((profile) => {
    console.log('We got a profile', profile);
  })
  .catch((error) => {
    console.error('We got an error', error)
  });

Our code above defines a function that takes a user ID and returns a promise (the return type of axios.get().then().catch()). It calls an HTTP endpoint to get profile information and will resolve with a profile or reject with an error. This works fine, but there is a lot of syntax inside getUserProfile that clutters our actual business logic:

  .then((response) => {
     /* important stuff */
  })
  .catch((error) => {
     /* more important stuff */
  });

JavaScript async/await step by step

Enter async/await. These functions let you work with promises without all of the then and catch syntax and makes your asynchronous code readable. When added before an expression that evaluates to a promise, await will wait for the promise to resolve, after which the rest of the function will continue executing. The await function can only be used inside async functions, which are those preceded by the async operators. If a promise is fulfilled with a value, you can assign that value like so:

let someFunction = async () => {
  let fulfilledValue = await myPromise();
};

If the promise is rejected with an error, the await operator will throw the error.

Let's rewrite the getUserProfile function step-by-step using async/await.

1. Add the async keyword to our function, making it an asynchronous function. This allows us to use the await operator in the function body.

let getUserProfile = async (userId) => {
  /* business logic will go here */
};

2. Use the await keyword on our promise and assign the resolved value:

let getUserProfile = async (userId) => {
  let response = await axios.get(`/user/${userId}`);
  /* more to do down here */
}

3. We want to return the data property of our resolved response value. Instead of having to nest it within a then block, we can simply return the value.

let getUserProfile = async (userId) => {
  let response = await axios.get(`/user/${userId}`);
  return response.data;
  /* more to do down here */
}

4. Add error handling. If our HTTP fails and the promise gets rejected, the await operator will throw the rejected message as an error. We need to catch it and re-throw our own error.

let getUserProfile = async (userId) => {
  try {
    let response = await axios.get(`/user/${userId}`);
    return response.data;
  } catch (error) {
    // Do something smarter with `error` here
    throw new Error(`Could not fetch user profile.`);
  }
};

JavaScript async/await gotchas

We’ve cut down on the amount of syntax we use by a few characters, but more importantly we can read through our code line-by-line as if it were synchronous code. There are, however, some sticky spots that you should be aware of.

Adding async quietly changes your function’s return value. I’ve seen a lot of confusion resulting from turning to async/await when needing to add asynchronous calls to a previously synchronous function. You might think you can add async and await and everything else will continue working as expected, but async functions return promises. So if you change a function to be asynchronous, you need to make sure that any existing function calls are adjusted to appropriately handle a promise. To be fair, checking pre-existing function calls would also be a thing you need to do if you use traditional promise syntax, but async functions aren’t as obvious about it.

Using async/await means having to deal with a promise... somewhere. Once you’ve added an asynchronous function somewhere, you’re in promise land. You will need to make sure any functions that call the async function either handle the result as a promise or use async/await themselves. If the async function is deeply nested, then the stack of functions that lead to that function call might also be async functions. Again, this issue isn’t specific to a async/await and would be a problem with promises as well. However, at some point, you need to have a promise. If you keep bubbling async/await up through the call stack, eventually you get to the global context where you can’t use await. Somewhere you’ll have to deal with an async function in the traditional, promise way.

Using async/await requires also using try/catch. If you’re using async to handle any promise that could possibly be rejected, you’re going to have to use try/catch. This is typically not a frequently used language feature. I’ve seen it used almost exclusively within libraries where the only way to detect certain features is by trying them out and catching errors accordingly. This is, of course, a broad generalization, but my point is that it’s possible to write solid, idiomatic JavaScript for years without having to use try/catch. Adopting async/await requires you to also adopt try/catch.

While there are several drawbacks to using async/await, it can greatly improve the readability of code that relies on promises. Next week, we’ll level up and cover some of the details around promises that lead to the most confusion. As always, reach out to me on Twitter with any comments or questions.

Copyright © 2018 IDG Communications, Inc.