How to use async and await in JavaScript

JavaScript’s async and await functions make for readable and maintainable asynchronous code. Just watch out for their downsides.

Asynchronous JavaScript: How to use async and await
Monkey Business Images/Shutterstock

One of the nicest improvements to developer experience in recent JavaScript history is the introduction of the async and await keywords, introduced in ES2017. Together, these elements make it easy to write asynchronous logic with a simple syntax. Under the hood, they use promises, but for many cases, async and await will help you write more readable and maintainable asynchronous code without thinking about implementation. In this article, we'll first look at how to use async await in your JavaScript programs. Then, we'll talk about some of the downstream implications of using the syntax.

JavaScript async and await defined

To start, let's get a working definition of the async and await JavaScript keywords:

  • async: Declares a function as one that will contain the await keyword within it.
  • await: Consumes an asynchronous function that returns a promise with synchronous syntax.

Consuming a JavaScript promise

Possibly the most common use case in all of JavaScript for async and await is using the browser’s built-in fetch API. Listing 1 declares an async function and uses await within it. The purpose is to pull some JSON data from the public Star Wars API.

Listing 1. JavaScript async/await and the fetch API


async function getStarWarsData() {
  try {
    const response = await fetch('https://swapi.dev/api/people/1/');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

getStarWarsData();
// Returns: {name: 'Luke Skywalker', height: '172', mass: '77', hair_color: 'blond', skin_color: 'fair', …}

Notice that you can get hold of the promise returned by a method like fetch(); in this case, the response object. response.json() is also an asynchronous method returning a promise, so we again use await

Our getStarWarsData() function must be prefixed with the async keyword— otherwise, the browser will not allow await in its body.

Refactoring with async and await

Now, let's look at a more complex example. To start, we’ll lay out a simple JavaScript program using promises, then we'll refactor it to use async and await. In our example, we'll use axios, a promise-based HTTP library. Here's the relevant snippet:

Listing 2. The promise-based example using axios


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)
  });

Listing 2 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 the abundance of syntax inside getUserProfile clutters our business logic, as shown in Listing 3.

Listing 3. Clutter in the getUserProfile method


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

Now, let's see what we can do to simplify this program.

Step by step with async/await

The async and await functions let you work with promises without all of the then and catch syntax promises usually require. In essence, they make your asynchronous code readable. The async and await keywords let you write code that behaves asynchronously but reads synchronously. Whenever the JavaScript interpreter sees an await, it’ll stop execution, go perform the task, and then return as though it were a normal call. This makes for code that is readable and clean, but it does limit the ability to orchestrate complex concurrency. In those cases, you need access more directly to the promises that underlie async/await and the initiation and resolution of the tasks.

When added before an expression that evaluates to a promise, await waits for the promise to resolve, after which the rest of the function continues executing. The await function can only be used inside async functions, which are the functions preceded by the async operators. If a promise is fulfilled with a value, you can assign that value like so:

Listing 4. Assigning a value to a promise


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 and await.

Step 1. Add the async keyword to our function

Adding the async keyword makes a function asynchronous. This allows us to use the await operator in the function body:


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

Step 2. Add the await keyword to the promise

Next, we 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 */
}

Step 3. Return the value

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 */
}

Step 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. But we do still need to watch out for the following sticky spots.

Adding async quietly changes your function’s return value

I’ve seen a lot of confusion resulting from turning to async/await in order 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. The issue is that 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 asynchronous functions aren’t as obvious about it.

Using async/await means having to deal with a promise

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 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 also use try/catch, which is 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 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.

Conclusion

This article introduced the async and await keywords in JavaScript. I've shown you how this syntax is used in both a simple and a more complex example involving promises. I've also discussed some of the gotchas to look out for when using async/await in JavaScript programs.

While there are a few drawbacks to using async/await, it can greatly improve the readability of code that relies on promises.

Copyright © 2023 IDG Communications, Inc.