How to escape async/await hell
Async /await frees us from callback hell, but there are also complaints about async/await. Because what follows is the birth of async/await hell.
In this article I will try to explain what async/await hell is, and I will also share some ways to avoid them.
What is async/await hell?
When writing asynchronous JavaScript code, people often add the await keyword in front of function call after function call. This can cause performance problems because normally, the execution of a statement does not depend on the execution of the previous statement, but with the await keyword added, you still need to wait for the execution of the previous statement to finish before you can execute a statement.
An example of async/await hell.
Suppose you write a piece of code to buy a pizza and a drink that looks like this.
(async() = > {const pizzaData = await getPizzaData() // async call
const drinkData = await getDrinkData() // async call
const chosenPizza = choosePizza() // sync call
const chosenDrink = chooseDrink() // sync call
await addPizzaToCart(chosenPizza) // async call
await addDrinkToCart(chosenDrink) // async call
orderItems() // async call}) ()Copy the code
On the face of it, this code looks syntactically correct and runs. However, this is not a good implementation because it excludes concurrent execution. Let’s take a look at what this code does so that we can be more specific about the problem.
explain
We wrapped this code in an asynchronous, immediate-execution function. The following things will happen in order:
- Get a list of pizzas.
- Get a list of drinks.
- Select a pizza from the pizza list.
- Select a drink from the drink list.
- Add the selected pizza to your cart
- Add the selected beverage to your cart.
- Make sure the order
What went wrong?
As I mentioned earlier, all statements are executed line by line; there is no concurrent execution. Let’s think for a moment, why do we need to wait for the pizza list to return before fetching the drink list? We should try to get a list of drinks and pizzas at the same time. However, when we need to select a pizza, we need to get the list of pizzas first. The same goes for drinks.
Therefore, we determined that the pizza related work and the beverage related work could be performed simultaneously, but each step of the pizza related work should be performed in sequence.
Another bad example
This JavaScript code retrieves the items in the shopping cart and sends a request to confirm the order.
async function orderItems() {
const items = await getCartItems() // async call
const noOfItems = items.length
for(var i = 0; i < noOfItems; i++) {
await sendRequest(items[i]) // async call}}Copy the code
In this case, the for loop needs to wait for the current sendRequest() execution to complete before executing the next loop. In reality, however, we don’t need to wait. We want to send all requests as quickly as possible and wait for them to complete.
I hope you now have a clear understanding of what async/await hell is and how badly they affect the performance of your application. Now, I want to ask you a question
What happens if we forget the await keyword?
If you forget to add the await keyword before an asynchronous function call, then the function is executing, which means that await is not necessary for the function to execute. This asynchronous function returns a promise that we can use later.
(async() = > {const value = doSomeAsyncTask()
console.log(value) // an unresolved promise}) ()Copy the code
As a result, the compiler does not know that you need to wait for the function to complete, so the compiler will exit the program before the asynchronous task completes. So we need the await keyword.
One interesting property of promises is that you can get a promise in previous code, and then wait for the promise to be fulfilled in later code. This is the key to getting out of async/await hell.
(async() = > {const promise = doSomeAsyncTask()
const value = await promise
console.log(value) // the actual value}) ()Copy the code
As you can see,doSomeAsyncTask() returns a promise. At this point,doSomeAsyncTask() has already started executing. To get the result of the promise, we can add await before the promise, and JavaScript will immediately stop executing the next line of code until we get the return value of the promise.
How to escape async/await hell?
You should follow these steps to escape async/await hell.
Find all statements that have been executed by other statements
In our first example, we’re choosing a pizza and a drink. So we came to the conclusion that before we could choose a pizza, we needed a list of pizzas. Meanwhile, we need to select the pizza before adding it to our shopping cart. We can think of these three steps as interdependent, and we can’t perform the next task until the previous one is complete. But if we broaden our horizons, we’ll see that choosing a pizza doesn’t depend on choosing a drink — we can choose both. That’s what machines can do better than we can. So far, we have found that some statements depend on other statements to execute, but others do not.
Integrate interdependent statements into asynchronous functions.
As we can see, choosing a pizza requires several interdependent statements, such as getting a pizza list, selecting one of the pizzas and adding it to the cart. We should combine these statements in an asynchronous function. This will give us two asynchronous functions,selectPizza() and selectDrink()
Execute these asynchronous functions concurrently.
We will take advantage of the Event loop to execute these non-blocking asynchronous functions concurrently. To do this, a common approach is to return a promise and then use the promise.all method.
Let’s correct this example
Following the three steps mentioned earlier, we applied them to our example.
async function selectPizza() {
const pizzaData = await getPizzaData() // async call
const chosenPizza = choosePizza() // sync call
await addPizzaToCart(chosenPizza) // async call
}
async function selectDrink() {
const drinkData = await getDrinkData() // async call
const chosenDrink = chooseDrink() // sync call
await addDrinkToCart(chosenDrink) // async call
}
(async() = > {const pizzaPromise = selectPizza()
const drinkPromise = selectDrink()
await pizzaPromise
await drinkPromise
orderItems() // async call}) ()// I prefer the following implementation.
(async() = > {Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call}) ()Copy the code
We have now combined these statements into two functions, where the execution of each statement depends on the execution of the previous function. We then execute selectPizza() and selectDrink() concurrently.
In the second example, we need to resolve an unknown number of promises. Solving this situation is simple: we just create an array and store the promises in it. Then, using the promise.all () method, you can concurrently wait for all promises to return results.
async function orderItems() {
const items = await getCartItems() // async call
const noOfItems = items.length
const promises = []
for(var i = 0; i < noOfItems; i++) {
const orderPromise = sendRequest(items[i]) // async call
promises.push(orderPromise) // sync call
}
await Promise.all(promises) // async call
}
// I prefer the following implementation
async function orderItems() {
const items = await getCartItems() // async call
const promises = items.map((item) = > sendRequest(item))
await Promise.all(promises) // async call
}
Copy the code
I like how this article helps you get out of the async/await base and improve your application performance. If you like this post, please like it and bookmark it.