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 theawait
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.