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:
It causes the return value of the async function to be a Promise resolving with the return value of the original, passed function.
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:
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 aResponse
object. So this is the value we assign toresponse
.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.
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 withtransformedJson
.
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.