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.