How to escape Async /await hell

Async /await saves us from callback hell, but if we abuse it we fall into async/await hell.

In this article I will explain what async/await hell is and share a few tricks to avoid it.

What is await/async hell

In asynchronous Javascript programming, we usually write a lot of async methods and wait for them with await keyword. There are many times when the next line execution does not depend on the previous line, but we still wait with await, which may cause some performance problems.

An example of await/async hell

How to write a pizza and drink order code? It might look something 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

It looks fine and it works, but it’s not a good implementation. Let’s take a look at what this code does to locate the problem.

explain

We’ll wrap our code around async IIFE, and the following will execute in sequence.

  1. Get the pizza menu
  2. Get beverage menu
  3. Select pizza from the pizza menu
  4. Select a beverage from the beverage menu
  5. Add the chosen pizza to your cart
  6. Add the selected drinks to your cart
  7. Place the order

What’s wrong?

As I just emphasized, these statements are executed sequentially, without concurrency. Come to think of it, why do I have to get the pizza menu before I get the drink menu? I should get both menus at the same time. Of course, get the pizza menu before you choose the pizza, and the same rule applies to drinks.

So we can conclude that pizza-related work and drink-related work can be done in parallel, but the various steps involving pizza-related work need to be done sequentially (step by step).

Another bad example

This code retrieves the shopping item in the shopping cart and issues an order request.

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 example, the for loop must wait for the last sendRequest() to complete before the next iteration, but we don’t need to wait at all. We just want to send requests as quickly as possible and wait for them to complete.

Presumably by now you know what async/await hell is and how bad it is for performance. Now I want to ask you a question.

What if you forget the await keyword?

If you forget to use await, the async function executes and returns a Promise, which you can resolve later.


(async () => {
  const value = doSomeAsyncTask()
  console.log(value) // an unresolved promise
})()Copy the code

Another consequence is that the compiler does not know that you want the function to execute completely, so the compiler will exit the program without completing the asynchronous function, so you still need to use the await keyword

Promises one interesting feature of Promises is that you can get promises in one line of code and wait and resolve in another line. This is the key to avoiding async/await hell.

(async () => {
  const promise = doSomeAsyncTask()
  const value = await promise
  console.log(value) // the actual value
})()Copy the code

As you can see, the doSomeAsyncTask() method returns a Promise that has already been executed when called. To get its parse value, we use the await keyword to tell the compiler to wait until the parse is complete before executing the next line.

How to avoid async/await hell

You should follow these steps to avoid async/await hell:

Find the statement dependencies

In the first example, we chose a pizza and a drink. To summarize, the pizza menu must be obtained before the pizza is selected, and the pizza menu must be selected before the pizza is added to the cart. These three steps are interdependent, and the next step must be completed after the previous step.

Choosing a drink doesn’t depend on choosing a pizza, so choosing a pizza and a drink can be done in parallel. And that’s one thing machines can do better than we can.

Encapsulate asynchronous methods that are interdependent

As you can see, the pizza selection depends on getting the pizza menu, selecting, and adding to the cart. So we put these dependencies in an asynchronous method, and the same goes for drinks, which is why we have selectPizza() and selectDrink() asynchronous methods.

Parallel execution

We use an event loop to execute these asynchronous methods in parallel without blocking. Two common ways to do this are to return promises as soon as possible and use promise.all ().

Let’s fix the code and apply these three methods to our example.

Let’s change the code

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 })() // Although I prefer it this way (async () => { Promise.all([selectPizza(),  selectDrink()]).then(orderItems) // async call })()Copy the code

We wrapped the interdependent statements in separate functions and now execute selectPizza() and selectDrink() at the same time

In the second example, we need to deal with an unknown number of promises. Promises are easy to handle. Let’s put Promises in an array, then use promise.all () to let Promises run in parallel, and wait for them all to be executed.

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 } // Although I prefer it this way 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 hope this article will help you think about using async/await and improve the performance of your application.