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
awaitexpression 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
fetchcall returns a promise:const response = await fetch("https://api.example.com/v1/posts/1");The presence of
awaitcauses 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 aResponseobject. 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
fetchAndTransformJsonis 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.