Handling Nested Promises Using Async/Await in React
This guide explains how to take advantage of async/await to simplify nested promises in a React app, cutting down on asynchronous code and mental complexity.
Sep 25, 2020 • 6 Minute Read
Introduction
Asynchronous code always carries cognitive overhead with it. It may be that the human mind is used to thinking synchronously and so trying to make sense of asynchronous code can seem daunting. There have been many constructs throughout the years that try to mitigate this complexity. For a long time, the only way to model asynchronicity in JavaScript code was via callbacks. The problem with callbacks was that they left a trail of "spaghetti" code through your application—you may be familiar with the term "callback hell".
Modern JavaScript introduced a new way of modeling asynchronous code: Promises. Promises gave JavaScript and React developers the ability to write asynchronous code without callbacks—however, it is still easy to nest promises in the old style, and this can lead to hard-to-read code.
The new async/await keywords introduced into modern JavaScript aim to solve this problem by providing "syntactic sugar" on top of promises. In this guide, you will learn how to take advantage of async/await in order to simplify nested promises in your React app. You will see how you can not only cut the amount of asynchronous code you write in half but also remove most of the mental complexity that is involved. You will see how async/await syntax allows you to reason about asynchronous code in a synchronous manner.
Let's get started.
Problem Overview: Nested Promises
It is not uncommon to see code written in the following fashion. Below, you can see some typical asynchronous code that is using nested promises in order to make successive HTTP requests to get "fish and chips". Most of the time, the reason you will see code written like this is that often it is your first instinct in writing asynchronous code.
// my-component.jsx
// We have to get chips after we get fish...
getFishAndChips = () => {
fetch(this.fishApiUrl) // Request fish
.then(fishRes => {
fishRes.json().then(fish => {
this.fish = fish;
const fishIds = fish.map(fish => fish.id);
fetch( // Request chips using fish ids
this.chipsApiUrl,
{
method: 'POST',
body: JSON.stringify({ fishIds })
}
)
.then(chipsRes => {
chipsRes.json().then(chips => {
this.chips = chips;
})
})
})
})
}
The above code nests two fetch calls in order to ensure that you request chips after you request fish. This is because, in order to request chips, you need to send an array of fish IDs with the POST request. This works, however, there is a huge readability problem here. Writing the above code is akin to trading callback hell for ... promise hell! Who wants to do that!? It is possible to dramatically reduce the amount of code needed for this feature. Let's take a look at promise chaining!
Interim Solution: Promise Chaining
The first way to remove the nesting of these fetch calls is to use what is known as promise chaining. Promise chaining can be achieved by simply returning another promise from within a call to Promise.then. The following example shows how to do this:
// We have to get chips after we get fish...
getFishAndChips = () => {
fetch(this.fishApiUrl) // Request fish
.then(response => response.json())
.then(fish => {
this.fish = fish;
const fishIds = fish.map(fish => fish.id);
return fetch( // Request chips using fish ids
this.chipsApiUrl,
{
method: 'POST',
body: JSON.stringify({ fishIds })
}
);
})
.then(response => response.json())
.then(chips => {
this.chips = chips;
});
}
This is great! The code is much more maintainable, succinct, and readable. But we can do even better! With the new async/await syntax that has been brought into JavaScript, we can remove the use of multiple calls to Promise.then entirely! You will see how this can be achieved in the next section.
The Optimal Solution: Async/Await
The async/await keywords are a wonderful mechanism for modeling asynchronous control-flow in computer programs. In JavaScript, these keywords are syntactic sugar on top of Promises—they abstract away the calls to Promise.then. In the following code, we refactor the getFishAndChips function to use async/await.
// We have to get chips after we get fish...
getFishAndChips = async () => {
const fish = await fetch(this.fishApiUrl).then(response => response.json());
const fishIds = fish.map(fish => fish.id),
chipReqOpts = { method: 'POST', body: JSON.stringify({ fishIds }) };
const chips = await fetch(this.chipsApiUrl, chipReqOpts).then(response => response.json());
}
Wow! Compare the first code snippet from the first section with this code. What a difference! We have dramatically condensed the code used while achieving the same effect. This code is easier to read and maintain. You can easily see how a lot of the mental complexity is reduced by being able to reason about the code in a synchronous fashion—no nesting required! All that is needed is to mark your async function with the async keyword. This enables you to use the await keyword to resolve promises for you! The async/await mechanism for control flow is an extremely powerful way to reason about anything asynchronous within your app.
Conclusion
It can be difficult to compose chained sequences of asynchronous actions within your app. Nesting promises is an unfortunate byproduct of implementing this. But thankfully, as you've seen in this guide, avoiding and/or refactoring nested promises is easily achieved via either promise chaining or async/await syntax. Of these two solutions, the most readable code was written using async/await syntax, so strive to use this in your own app!
You can now be confident in writing modern and readable asynchronous code within your React components! For more information, check out the documentation for async/await syntax.