Construction of async / await in JavaScript: strengths, pitfalls and features of use

The async / await design appeared in the ES7 standard. It can be considered a remarkable improvement in the field of asynchronous programming in JavaScript. It allows you to write code that looks like synchronous, but is used to solve asynchronous tasks and does not block the main thread. Despite the fact that async / await is a great new language feature, it is not so easy to use it correctly. The material, the translation of which we publish today, is devoted to a comprehensive study of async / await and a story about how to use this mechanism correctly and efficiently.

image

Async / await strengths


The most important advantage that a programmer who uses the async / await construct has is that it allows you to write asynchronous code in the style characteristic of synchronous code. Compare code written using async / await and promis-based code.

// async/await async getBooksByAuthorWithAwait(authorId) {  const books = await bookModel.fetchAll();  return books.filter(b => b.authorId === authorId); } //  getBooksByAuthorWithPromise(authorId) {  return bookModel.fetchAll()    .then(books => books.filter(b => b.authorId === authorId)); } 

It is easy to see that the async / await version of the example is clearer than its version, which uses promise. If you do not pay attention to the await keyword, this code will look like a normal set of instructions that are executed synchronously - as in usual JavaScript or in any other synchronous language like Python.

The appeal of async / await is provided not only by improving the readability of the code. This mechanism also enjoys excellent browser support, which does not require any workaround. So, today asynchronous functions fully support all major browsers.


All major browsers support asynchronous functions ( caniuse.com )

This level of support means, for example, that code using async / await does not need to be transported . In addition, it facilitates debugging, which is perhaps even more important than the lack of need for transpilation.

The following figure shows the process of debugging an asynchronous function. Here, when setting a breakpoint on the first instruction of the function and executing the Step Over command, when the debugger reaches the line in which the await keyword is used, you can notice how the debugger pauses briefly, waiting for the bookModel.fetchAll() function to bookModel.fetchAll() , and then goes to the line where the .filter() command is .filter() ! Such a debugging process looks much simpler than debugging promises. Here, when debugging a similar code, I would have to set another breakpoint in the .filter() .


Debug asynchronous function. The debugger will wait for the execution of the await line and will go to the next line after the operation is completed

Another strength of the mechanism under consideration, which is less obvious than what we have already considered, is the presence of the async keyword here. In our case, using it guarantees that the value returned by the getBooksByAuthorWithAwait() function will be promise. As a result, in the code that calls this function, you can safely use the getBooksByAuthorWithAwait().then(...) await getBooksByAuthorWithAwait() or await getBooksByAuthorWithAwait() . Consider the following example (note that it is not recommended to do this):

 getBooksByAuthorWithPromise(authorId) { if (!authorId) {   return null; } return bookModel.fetchAll()   .then(books => books.filter(b => b.authorId === authorId)); } } 

Here, the getBooksByAuthorWithPromise() function can, if everything is normal, return a promise, or if something went wrong - null . As a result, if an error occurred, you cannot safely call .then() . When declaring functions using the async errors of this kind are impossible.

About the wrong perception of async / await


In some publications, the async / await construct is compared with promises and suggests that it represents a new generation of the evolution of asynchronous programming in JavaScript. With this I, with all due respect to the authors of such publications, let me disagree. Async / await is an improvement, but it is nothing more than "syntactic sugar", the appearance of which does not lead to a complete change in the programming style.

In essence, asynchronous functions are promises. Before a programmer can properly use an async / await construct, he should have a good look at promises. In addition, in most cases, working with asynchronous functions, you need to use promises.

Take a look at the getBooksByAuthorWithAwait() and getBooksByAuthorWithPromises() functions from the above example. Please note that they are identical not only in terms of functionality. They also have exactly the same interfaces.

All this means that if you call getBooksByAuthorWithAwait() directly, it will return a promise.

In fact, the essence of the problem that we are talking about here is the wrong perception of a new design, when a deceptive feeling is created that the synchronous function can be converted into asynchronous thanks to the simple use of the keywords async and await and nothing more.

Async / await pitfalls


Let's talk about the most common mistakes that can be made using async / await. In particular, the irrational use of sequential calls of asynchronous functions.

Although the await keyword can make the code look like synchronous, using it is worth remembering that the code is asynchronous, which means you need to be very careful about the sequential call of asynchronous functions.

 async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

This code, in terms of logic, seems to be correct. However, there is a serious problem. Here is how it works.

  1. The system calls await bookModel.fetchAll() and waits for the completion of the .fetchAll() command.
  2. After getting the result from bookModel.fetchAll() , an await authorModel.fetch(authorId) call will be made.

Note that the call authorModel.fetch(authorId) does not depend on the results of the call bookModel.fetchAll() , and, in fact, these two commands can be executed in parallel. However, using await causes these two calls to be performed sequentially. The total time of the sequential execution of these two commands will be more than the time of their parallel execution.

Here is the correct approach to writing such code:

 async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Consider another example of the misuse of asynchronous functions. It's still worse than the previous example. As you can see, in order to asynchronously load a list of certain elements, we need to rely on the possibilities of promises.

 async getAuthors(authorIds) { //  ,     // const authors = _.map( //   authorIds, //   id => await authorModel.fetch(id)); //   const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); } 

If in a nutshell, then, in order to correctly use asynchronous functions, you need, as in times when this opportunity was not, first think about asynchronous execution of operations, and then write code using await . In difficult cases, it will probably be easier to just directly use promises.

Error processing


When using promises, the execution of an asynchronous code can be completed either as expected - then they say about the successful resolution of the promis, or with an error - then they say that the promis is rejected. This gives us the opportunity to use, respectively, .then() and .catch() . However, error handling when using the async / await mechanism can be difficult.

Try try / catch design


The standard way to handle errors when using async / await is to try / catch. I recommend using this approach. When an await call is made, the value returned when the promise is rejected is represented as an exception. Here is an example:

 class BookModel { fetchAll() {   return new Promise((resolve, reject) => {     window.setTimeout(() => { reject({'error': 400}) }, 1000);   }); } } // async/await async getBooksByAuthorWithAwait(authorId) { try { const books = await bookModel.fetchAll(); } catch (error) { console.log(error);    // { "error": 400 } } 

The error captured in the catch is just the value that results when the promise is rejected. After catching the exception, we can apply several approaches to working with it:


Here are the advantages of using the try / catch construct:


It should be noted that there is one drawback to the try / catch mechanism. Since try / catch intercepts any exceptions that occur in a try block, exceptions that are not related to promises will also get into the catch handler. Take a look at this example.

 class BookModel { fetchAll() {   cb();    //    ,   `cb`  ,       return fetch('/books'); } } try { bookModel.fetchAll(); } catch(error) { console.log(error);  //       "cb is not defined" } 

If you run this code, you can see the ReferenceError: cb is not defined error message in the console ReferenceError: cb is not defined . This message is output by the console.log() command from the catch , and not by JavaScript itself. In some cases, such errors lead to serious consequences. For example, if the call is bookModel.fetchAll(); it is hidden deep in a series of function calls and one of the calls “swallows” the error, it will be very difficult to detect such an error.

▍Return functions of two values


The inspiration for the next way to handle errors in asynchronous code was Go. It allows asynchronous functions to return both an error and a result. Read more about it here .

In a nutshell, asynchronous functions, with this approach, can be used as follows:

 [err, user] = await to(UserModel.findById(1)); 

Personally, I don’t like it, since this error-handling method introduces Go programming style into JavaScript, which looks unnatural, although in some cases it can be very useful.

▍Using .catch


The last error handling we’ll talk about is using .catch() .

Recall how await works. Namely, the use of this keyword causes the system to wait until the promise completes its work. Also, remember that the promise.catch() view promise.catch() also returns a promise. All this suggests that you can handle errors in asynchronous functions like this:

 // books   undefined   , //    catch     let books = await bookModel.fetchAll() .catch((error) => { console.log(error); }); 

There are two minor problems with this approach:


Results


The async / await construct, which appeared in ES7, is definitely an improvement to the asynchronous programming mechanisms in JavaScript. It can facilitate reading and debugging code. However, in order to use async / await correctly, you need a deep understanding of promises, since async / await is just a “syntactic sugar” based on promises.

Hopefully, this material has allowed you to become more familiar with async / await, and what you learned here will save you from some common mistakes that occur when using this design.

Dear readers! Do you use the async / await construct in javascript? If so, please tell us how you handle errors in asynchronous code.

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


All Articles