Async/Await: a Game Changer for Haters of Promise Chaining
If you became a frontend engineer after 2015, chances are that you have used or at least heard about Promise
. As I have covered in my previous blog, Promise
can help us specify sequential relations between operations in asynchronous programming in a readable and maintainable manner. Promise
object in JavaScript has methods, such as then
and catch
, that can help us organize the sequential execution of operations in a pattern called Promise Chaining. Clean and organized as Promise Chaining is, some people—especially those more accustomed to synchronous programming—find it unintuitive and confusing. Lucky for them, ECMAScript 2017 introduced a new feature that allows us to write asynchronous code as if it were synchronous. This new feature consists of two keywords: async
and await
. In this article, I will cover the fundamentals of async/await
and how to transition from Promise Chaining to using async/await
.
Await: a Wait for a Promise to Be Fulfilled
I will start with the keyword await
. We use await
when invoking a function that returns a Promise
object. It blocks all subsequent operations until the completion (or failure) of this Promise
object. It can also help assign the Promise
object’s fulfillment value to a constant or a variable. The following is an example to illustrate its primary usage.
1 2 3 4 5 6 7 |
function getNumberOneInOneSecond() { return new Promise((resolve) => { setTimeout(() => { resolve(1) }, 1000) }) } |
Let’s say we have a function called getNumberOneInOneSecond
that returns a Promise
object. When constructing this Promise
object, we pass to the constructor an anonymous function whose argument is a resolve function. In the anonymous function, we use setTimeOut
to invoke the resolve
function with number one as the argument after one second.
If we initialize a constant with the returned value of getNumberOneInOneSecond
, the constant will be an instance of Promise
.
1 2 3 4 5 6 7 8 9 10 |
function getNumberOneInOneSecond() { return new Promise((resolve) => { setTimeout(() => { resolve(1) }, 1000) }) } const numberOnePromise = getNumberOneInOneSecond() console.log(numberOnePromise instanceof Promise) |
However, intuitively, we expect the constant to be number one. This is where the await
keyword comes in handy. We can use the await
keyword to directly assign the fulfillment value of the Promise
object to the constant numberOne
. As mentioned above, await
blocks all subsequent operations when the Promise
object is still pending. Therefore, the console prints constant numberOne
only after the Promise
object is fulfilled.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function getNumberOneInOneSecond() { return new Promise((resolve) => { setTimeout(() => { resolve(1) }, 1000); }) } async function main() { console.log('Getting number one...') // Executed immediately const numberOne = await getNumberOneInOneSecond() console.log(numberOne) } main() |
Async: a Sanctuary for Await and a Factory Producing Promises
The await
keyword helps us write synchronous-ish code in asynchronous programming, but we cannot use it wherever we want. You might be confused regarding why I decided to wrap await getNumberOneInOneSecond()
in the main
function and mark it with async
in the code example above. Well, I certainly did not do it just for fun. I had to do it because we can only use the await
keyword in functions marked as async
.
In addition, async
can transform a regular function into one that returns a Promise
object. Let’s say we have a function called multiplyByTwo
. It takes in one argument. First, it checks if the argument is a number. If the argument is a number, then multiplyByTwo
multiplies it by two and returns the product. Otherwise, multiplyByTwo
throws an error.
1 2 3 4 5 6 7 8 9 |
function multiplyByTwo(input) { if (typeof input === "number") { const output = input * 2 return output } else { const error = new Error("Invalid Input") throw error } } |
If we add the async
keyword ahead of the function, then it becomes a function that returns a Promise
object. The returned Promise
object is resolved with the return value of the original function and rejected with the error thrown by the original function.
1 2 3 4 5 6 7 8 9 |
async function multiplyByTwo(input) { if (typeof input === "number") { const output = input * 2 return output } else { const error = new Error("Invalid Input") throw error } } |
It is equivalent to the following function, which I used as an example in the previous blog.
1 2 3 4 5 6 7 8 9 10 11 |
function multiplyByTwo(input) { return new Promise((resolve, reject) => { if (typeof input === "number") { const output = input * 2 resolve(output) } else { const error = new Error("Invalid Input") reject(error) } }) } |
As we can see, the async
keyword allows us to write synchronous-looking functions in asynchronous programming. Not only can it liberate us from constructing Promise
objects every time we want to return a Promise in a function, but it can also help us avoid including unfamiliar functions like resolve
and reject
in our codebase.
Try/Catch: You Can Even Handle Errors in a Synchronous Style
At this point, you might ask, “How can we handle errors without using the catch method?” The answer is surprisingly simple! We can use our good old friend—the try/catch
construct.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
async function multiplyByTwo(input) { if (typeof input === "number") { const output = input * 2 return output } else { const error = new Error("Invalid Input") throw error } } async function main() { try { const output = await multiplyByTwo("2") console.log(output) } catch (e) { console.log(e) } } main() |
To handle the error that multiplyByTwo
might potentially throw, we wrap its invocation in a try
block and define the error-handling operations in a catch
block (in our case, we merely display the error).
Async/Await: Break Free from the Promise Chains
Next, I will demonstrate how we can use async/await to rewrite the following code example that utilizes Promise Chaining.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function addExclamationMark(input) { return new Promise((resolve, reject) => { if (typeof input === "string") { const output = input + "!" resolve(output) } else { const error = new Error("Invalid Input") reject(error) } }) } addExclamationMark("Hello, world") .then(addExclamationMark) // the same as .then((input) => addExclamationMark(input)) .then(addExclamationMark) .then(addExclamationMark) .then(addExclamationMark) .then((output) => console.log(output)) .catch((error) => console.log(error)) |
We first add the async
keyword in front of the definition of addExclamationMark
. Then, we remove the construction of the new Promise
object to unwrap the core operations. After that, we replace the invocations of resolve
and reject
with a return
and a throw
statement respectively.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
async function addExclamationMark(input) { if (typeof input === "string") { const output = input + "!" return output } else { const error = new Error("Invalid Input") throw error } } async function main() { try { const output1 = await addExclamationMark("Hello, world") const output2 = await addExclamationMark(output1) const output3 = await addExclamationMark(output2) const output4 = await addExclamationMark(output3) const output5 = await addExclamationMark(output4) console.log(output5) } catch (error) { console.log(error) } } main() |
To replace the chain of promises, we separate the five invocations of addExclamationMark
completely. We place await
ahead of each invocation of addExclamationMark
to assign the output to a new constant. Also, we pass the output of the previous invocation of addExclamationMark
as an argument to the next one. Last, we display the final output by calling console.log(output5)
.
As for the error handling operations previously wrapped in the catch
method, we now place them in a catch
block. Proceeding the catch
block is a try
block containing all the operations we want to perform if nothing goes wrong, including the five invocations of addExclamationMark
.
You Can Have the Best of Both Worlds
For those who do not want to fully commit to using async/await
, here is some good news: you CAN have the best of both worlds! Let’s say you are sold on the idea that using async
is a better way to define asynchronous functions (functions that return a Promise
object). However, you are unwilling to forfeit your freedom to use Promise Chaining. Instead of reluctantly inundating your codebase with the await
keyword, you can use async
with Promise Chaining. You can even mix await
with promise chains if it is your desired style. The world is your oyster!
In this article, I presented the fundamentals of async/await
—a powerful alternative to the usual way of organizing sequential execution of operations (Promise Chaining) in asynchonous programming. First, I explained how to use async/await
and the conditions for using them. Then, I demonstrated how to rewrite asynchronous code with async/await
. Last, I mentioned that it is totally acceptable to use async
and await
separately and selectively without absolute commitment. I hope this article has inspired you to start using, or at least consider using, async
and await
.