📜 ⬆️ ⬇️

Escape from hell async / await

More recently, the async / await construction in JavaScript looked like a great way to get rid of hell callbacks. However, careless use of async / await led to the emergence of a new hell.


The author of the material, the translation of which we are publishing today, talks about what hell is async / await, and how to escape from it.

What is hell async / await


In the process of working with asynchronous JavaScript capabilities, programmers often use constructs consisting of multiple function calls, each of which is preceded by the await keyword. Since often in such cases, expressions with await independent of each other, such code leads to performance problems. The point here is that before the system can deal with the next function, it needs to wait for the completion of the previous function. This is hell async / await.
')

Example: ordering pizza and drinks


Imagine that we need to write a script for ordering pizza and drinks. This script might look like this:

 (async () => { const pizzaData = await getPizzaData()    //   const drinkData = await getDrinkData()    //   const chosenPizza = choosePizza()    //   const chosenDrink = chooseDrink()    //   await addPizzaToCart(chosenPizza)    //   await addDrinkToCart(chosenDrink)    //   orderItems()    //   })() 

At first glance, the script looks quite normal, besides - it works as expected. However, upon careful consideration of this code, it turns out that its implementation is lame, since it does not take into account the features of asynchronous code execution. Having dealt with what is wrong here, we will be able to solve the problem of this script.

The code is wrapped in an asynchronous immediately called functional expression ( IIFE ). Please note that all tasks are performed exactly in the order in which they are given in the code, while in order to proceed to the next task, you need to wait for the previous one to complete. Namely, this is what happens here:

  1. Getting a list of pizza.
  2. Getting a list of drinks.
  3. Pizza selection from the list.
  4. Select a drink from the list.
  5. Adding the selected pizza to the basket.
  6. Add selected drink to cart.
  7. Checkout.

The above focuses on the fact that operations in the script are performed strictly sequentially. It does not use the possibility of parallel code execution. Consider the following: why do we expect to receive a list of types of pizza in order to start downloading the list of drinks? Should perform these tasks simultaneously. However, in order to be able to choose a pizza from the list, you must first wait for the pizza list to load. The same applies to the process of choosing a drink.

As a result, it can be concluded that tasks related to pizza and tasks related to drinks can be performed in parallel, but separate operations related only to pizza (or only to drinks) should be performed sequentially.

Example: placing an order based on the contents of the basket


Here is an example of code in which the data on the contents of the cart are loaded and the request to form an order is sent:

 async function orderItems() { const items = await getCartItems()    //   const noOfItems = items.length for(var i = 0; i < noOfItems; i++) {   await sendRequest(items[i])    //   } } 

In this case, the for loop has to wait for the completion of each call to the sendRequest() function in order to proceed to the next iteration. However, we do not really need this expectation. We want to complete all requests as quickly as possible, and then wait for them to complete.
I hope, now you are closer to understanding the essence of async / await hell, and how much it can affect application performance. Now think about the question posed in the heading of the next section.

What if you forget to use the await keyword?


If you forget to use the await keyword when calling an asynchronous function, the function will simply start executing. Such a function will return a promise that can be used later.

 (async () => { const value = doSomeAsyncTask() console.log(value) //   })() 

Another consequence of calling an asynchronous function without await is that the compiler will not know that the programmer wants to wait for the complete completion of the function. As a result, the compiler will exit the program without completing the asynchronous task. Therefore, do not forget about the await keyword where it is needed.

The promises have an interesting property: in one line of the code you can get a promise, and in the other - wait for its resolution. This fact is the key to escape from async / await hell.

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

As you can see, the call to doSomeAsyncTask() returns a promise. At this point, this function starts to run. In order to get the result of the promise resolution, we use the await keyword, telling the system that it should not immediately execute the following line of code. Instead, we must wait for the permission of the promise, and only then proceed to the next line.

How to get out of hell async / await?


To get out of hell async / await, you can use the following action plan.

â–Ť1. Find expressions that depend on the execution of other expressions.


In the first example, a script was shown for selecting a pizza and a drink. We decided that before we have the opportunity to choose a pizza from the list, we need to download a list of types of pizza. And before you add the pizza to the basket, you need to choose it. As a result, it can be said that these three steps depend on each other. You can not go to the next step without completing the previous one.

Now, if you think more broadly and think about drinks, it turns out that the process of choosing a pizza does not depend on the process of choosing a drink, so these two tasks can be parallelized. Computers do a very good job with parallel tasks.

As a result, we understood exactly which expressions depend on each other, and which ones do not.

â–Ť2. Group dependent expressions in separate asynchronous functions.


As we have already found out, the process of choosing a pizza consists of several steps: loading a list of types of pizza, choosing a specific pizza and adding it to the basket. It is these actions that should be collected into a separate asynchronous function. Not forgetting that a similar sequence of actions is typical for drinks, we come to two asynchronous functions, which can be called selectPizza() and selectDrink() .

â–Ť3. Run the resulting asynchronous functions in parallel


Now let us use the possibilities of the JavaScript event cycle in order to organize parallel non-blocking execution of the obtained asynchronous functions. There are two common patterns used here - early return of promises and the Promise.all() method.

Bug work


Let's apply in practice the three steps described above to get rid of hell async / await. Correct the above examples. This is how the first one will look now.

 async function selectPizza() { const pizzaData = await getPizzaData()    //   const chosenPizza = choosePizza()    //   await addPizzaToCart(chosenPizza)    //   } async function selectDrink() { const drinkData = await getDrinkData()    //   const chosenDrink = chooseDrink()    //   await addDrinkToCart(chosenDrink)    //   } (async () => { const pizzaPromise = selectPizza() const drinkPromise = selectDrink() await pizzaPromise await drinkPromise orderItems()    //   })() //    ,   ,      (async () => { Promise.all([selectPizza(), selectDrink()]).then(orderItems)   //   })() 

Pizza and drink related expressions are now grouped in the selectPizza() and selectDrink() functions. Inside these functions, the order of command execution is important, since the following commands depend on the results of the previous ones. Once the functions are prepared, we call them asynchronously.

In the second example, we have to deal with an unknown amount of promises. However, to solve this problem is very simple. Namely, it is necessary to create an array and put promises into it. Then, using Promise.all() , you can organize the pending resolution of all these promises.

 async function orderItems() { const items = await getCartItems()    //   const noOfItems = items.length const promises = [] for(var i = 0; i < noOfItems; i++) {   const orderPromise = sendRequest(items[i])    //     promises.push(orderPromise)    //   } await Promise.all(promises)    //   } 

Results


As you can see, what is called “async / await hell” at first glance looks quite decent, however, external well-being has a negative impact on performance. From this hell, however, it is not so difficult to escape. It is enough to analyze the code, find out what tasks can be solved using it, can be parallelized, and make the necessary changes to the program.

Dear readers! Have you ever seen hell async / await?

Source: https://habr.com/ru/post/353658/


All Articles