Translation from: Lightning Miners Translation Team

JavaScript async/await

By Charlee Li

Warehouse original link: issue

Translator: Xixi20160512

Async /await was introduced in the ES7 release and is a huge improvement over asynchronous programming in JavaScript. It allows us to process asynchronous processes synchronously without blocking the main thread. Using this feature well, however, may require a little ingenuity. In this article we will explore async/await from different angles and show how to use them correctly and efficiently.

The advantages of async/await

One of the biggest benefits of async/await is the synchronous programming style. Let’s look at an example:

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

Obviously, the async/await version is much easier to understand than the Promise version. If you omit the await keyword, this code looks like any other synchronous language (such as Python).

More than readability, async/await has native browser support. As of today, async functions are supported by all major browsers.

All major browsers support async functions. (Photo: Caniuse.com/)

Native support means you don’t need to compile code. More importantly, this will help with debugging. When you hit a breakpoint on the async entry and step into the await line, you will see that the debugger waits for bookModel.fetchall () to execute before proceeding to the next.filter line! This is much easier than the Promise example because you have to make another breakpoint on the.filter line.

Debug async functions. The debugger waits on the await line before moving to the next line.

Another, less obvious benefit is the async keyword. It declares the getBooksByAuthorWithAwait () method returns a promise, so the caller can like getBooksByAuthorWithAwait (). Then (…). Or await getBooksByAuthorWithAwait () call to security. Take a look at this example (bad practice) :

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

In the code above, getBooksByAuthorWithPromise may return a promise (normally) or null (special circumstances), returns null when the caller cannot safely call. Then (). This problem does not exist when async is used for declarations.

Async/await may be misleading

I disagree with some articles that compare async/await with Promise and say it’s the next generation solution in the evolution of asynchronous JavaScript programming. Async/await is an upgrade, but it is just a syntactic sugar and it will not completely change our programming style.

Essentially, async functions are still promises. You must understand Promises to use Async properly. Even worse, most of the time you must use promises and Async at the same time.

Think about the above example to use getBooksByAuthorWithAwait () and getBooksByAuthorWithPromises (). Notice that not only do they have the same functionality, but they also have the same interface.

This means that if you directly getBooksByAuthorWithAwait (), will return a promise.

Of course, this is not a bad thing. Only await gives people the feeling of “cool, this will convert an asynchronous function to a synchronous function” that is wrong.

Async/await trap

So what mistakes can be made using async/await? Here are some of the more common examples.

over-linearization

While await can make your code look like synchronous code, keep in mind that the code is still being executed asynchronously and be careful not to make your code too linear.

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

This code looks logically sound. However, it is not true.

  1. await bookModel.fetchAll()Will be waiting forfetchAll()Performed.
  2. thenawait authorModel.fetch(authorId)Before it gets executed

Note that authorModel.fetch(authorId) does not rely on the results of bookModel.fetchall (); in fact, they can be executed in parallel. However, since the two calls become serial with await, the total time will be much longer than the parallel method.

Here’s the correct way to use it:

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),  
    };
}
Copy the code

Or in more complicated cases, if you want to ask for a list in turn, you have to rely on Promises:

async getAuthors(authorIds) {  
    // WRONG, this will cause sequential calls 
    // const authors = _.map(  
    // authorIds,
    // id => await authorModel.fetch(id));
// CORRECT  
    const promises = _.map(authorIds, id= > authorModel.fetch(id));  
    const authors = await Promise.all(promises);
}
Copy the code

In short, you must think of the workflow as asynchronous and then try to write code synchronously using await. Promises may be easier to use directly under the complicated process.

Error handling

In promises, an asynchronous function returns two possible values: Resolved and Rejected. We can use.then() to handle normal cases. Catch () to handle exceptions. For async/await, however, exception handling can be a bit weird.

try… catch

The most standard (and recommended) approach is to use try… Catch expression. When await a function call, any value of Rejected is thrown as an exception. Here’s an example:

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

The caught error is the value of Rejected. After we catch this exception, we have a number of ways to handle it:

  • Handle the exception and return a normal value. (not incatchBlock to use anyreturnThe expression is equivalent to usingreturn undefined; In the meantime, an Resolved value is returned.)
  • Throw this exception if you want the caller to handle it. You can just throw the original error object, for examplethrow error;“, which allows you to use the promise chainasync getBooksByAuthorWithAwait()Methods (columns such as, you can still likegetBooksByAuthorWithAwait().then(...) .catch(error => ...)Call it this way); Or, you can useErrorObject wraps error objects, for example,throw new Error(error)In this way, all call stack records can be displayed in the console.
  • Use Reject, for example,return Promise.reject(error)This is equivalent tothrow errorTherefore, it is not recommended to use this method.

Using a try… The advantages of catch are as follows:

  • Simple, traditional. As long as you have experience with other languages, such as C++ or Java, you should have no difficulty understanding this approach.
  • You can put more than oneawaitThe call is wrapped in onetry... catchBlock to centrally handle all errors, if error handling for each step is not necessary.

There is a flaw in this approach. Due to try… Catch will catch all exceptions in this code block. Some other exceptions that would not normally be caught by Promises will also be caught. Consider this example:

class BookModel {  
    fetchAll() {    
        cb();    // note `cb` is undefined and will result an exception    
        return fetch('/books'); }}try {  
    bookModel.fetchAll();
} catch(error) {  
    console.log(error);  // This will print "cb is not defined"
}
Copy the code

Executing this code will give you an error in the console: ReferenceError: CB is not defined, the text is black. This error is printed by console.log() and not JavaScript itself. This can be fatal at some point: if the BookModel is wrapped up in a series of function calls, and one of them handles the error, it’s hard to find the error like this.

Causes the function to return two values at the same time

Another approach to error handling is inspired by the Go language. It allows async functions to return both an error value and a normal value. You can read more about it on the following blog:

How to write async await without try-catch blocks in Javascript*ES7 Async/await allows us as developers to write asynchronous JS code that look synchronous. In current JS version we… *blog.grossman.io

In short, you can use async functions like this:

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

I personally don’t like this approach because it brings the Go programming style to JavaScript, which isn’t natural, but it can be useful in some situations.

Using the catch

The final processing method I’ll cover is to still use.catch().

Recall the functionality of await: it waits for a promise to complete its task. Also recall that promise.catch() also returns a promise! So we can handle error handling like this:

// books === undefined if error happens,
// since nothing returned in the catch statement
let books = await bookModel.fetchAll()  
	.catch((error) = > { 
        console.log(error); 
    });
Copy the code

There are two secondary problems with this approach:

  • This is a mix of Promises and Async functions. You still need to understand how Promises work to understand them.
  • Error handling precedes normal flow, which is less intuitive.

conclusion

The async/await keyword introduced in ES7 is undoubtedly a major enhancement to asynchronous JavaScript programming. It makes code easier to read and debug. Then, in order to use them properly, one must fully understand Promises. Because promises are nothing but grammatical sugar. The underlying technology is still Promises.

Hopefully this article will give you some inspiration on async/await and help you avoid some common mistakes. Thanks for reading and please give me a thumbs up if you like.