📜 ⬆️ ⬇️

Javascript es8 and transition to async / await

We recently published material " Promises in ES6: Patterns and Anti-Patterns ". It caused a serious interest of the audience, in the comments to it our readers talked about the peculiarities of writing asynchronous code in modern JS projects. By the way, we advise you to read their comments - you will find a lot of interesting things there.

image

Following the advice of the user ilnuribat, we added a survey to the material, the purpose of which was to find out the popularity of promises, callbacks and async / await constructions. As of September 9, promises and async / await each received about 43% of the vote, with a small margin of async / await, and callbacks got 14%. The main conclusion that can be made by analyzing the survey results and comments is that all the available technologies are important, however, more and more programmers are async / await. Therefore, today we decided to publish a translation of an article about switching to async / await, which is a continuation of the material on promises.

Callbacks, promises, async / await


Last week, I wrote about promises , JS capabilities, which appeared in ES6. Promises were a great way to break out of the hell of callbacks . However, now, when support for async / await appeared in Node.js (from version 7.6.), I got the perception of promises as something like a temporary tool. I must say that async / await can also be used in browser code thanks to transpilers like babel .
')
I want to say that in this material I will use the latest features of JS, including template literals and pointer functions . View the list of ES6 innovations here .

Why is async / await great?


Until recently, asynchronous code in JavaScript, at best, looked awkward. For developers who switched to JavaScript from languages ​​such as Python, Ruby, Java, and practically any other, callbacks and promises seemed to be unnecessarily complicated constructions that are prone to errors and completely confuse the programmer.

The problem was that for the programmer there is not much difference between synchronous and asynchronous logic. There are a lot of problems regarding performance and optimization that a programmer needs to think about when he is writing asynchronous code, but a completely different syntax is too much.

Here are three examples that implement the same logic. The first uses the usual synchronous functions, the second - callbacks, the third - promises. Everyone solves the same problem: downloading information about the most popular article on HackerNews.

Here is a hypothetical example of a synchronous version:

 // :    ! let hn = require('@datafire/hacker_news').create(); let storyIDs = hn.getStories({storyType: 'top'}); let topStory = hn.getItem({itemID: storyIDs[0]}); console.log(`Top story: ${topStory.title} - ${topStory.url}`); 

It's all very simple - nothing new for anyone who wrote on JS. The code performs three steps: get a list of material IDs, download information about the most popular and display the result.

All this is good, but in JavaScript you cannot block the event loop . Therefore, if article identifiers and information about a popular article come from a file, come in the form of a response to a network request, if they are read from the database, or if they get into the program as a result of performing any resource-intensive input-output operation, the corresponding commands should always be done asynchronous, using callbacks or promises (which is why the above code will not work, in fact, our client for HackerNews is based on promises ).

Here is the same logic implemented on callbacks (an example, again, hypothetical):

 // :    ! let hn = require('@datafire/hacker_news').create(); hn.getStories({storyType: 'top'}, (err, storyIDs) => { if (err) throw err; hn.getItem({itemID: storyIDs[0]}, (err, topStory) => {   if (err) throw err;   console.log(`Top story: ${topStory.title} - ${topStory.url}`); }) }) 

Yeah. Now the code fragments that implement the functionality we need are nested inside each other and we need to align them horizontally. If there were 20 steps instead of three, then it would take 40 spaces to align the latter! And if you need to add a new step somewhere in the middle, you would have to re-align everything that is below it. This leads to huge and useless differences between different states of a file in Git. In addition, note that we must handle errors at every step of the whole structure. Grouping a set of operations in a single try / catch will not work.

Let's try to do the same now, using the promises:

 let hn = require('@datafire/hacker_news').create(); Promise.resolve() .then(_ => hn.getStories({storyType: 'top'})) .then(storyIDs => hn.getItem({itemID: storyIDs[0])) .then(topStory => console.log(`Top story: ${topStory.title} - ${topStory.url}`)) 

So, it already looks better. All three steps are equally aligned horizontally and adding a new step somewhere in the middle is as easy as inserting a new line. As a result, it can be said that the promise syntax is slightly verbose because of the need to use Promise.resolve() and because of all the .then() constructs present here.

Now, having understood the usual functions, callbacks and promises, let's see how to do the same with the async / await construction:

 let hn = require('@datafire/hacker_news').create(); (async () => { let storyIDs = await hn.getStories({storyType: 'top'}); let topStory = await hn.getItem({itemID: storyIDs[0]}); console.log(`Top story: ${topStory.title} - ${topStory.url}`); })(); 

This is much better! It looks like we did it as a synchronous code, except that the await keyword is used here. In addition, we placed the code in an anonymous function, declared with the async in order to make this code fragment better suited for further work with it.

It must be said that the methods hn.getStories() and hn.getItem() are designed so that they return promises. When executed, the event loop is not blocked. Thanks to async / await , for the first time in JS history, we were able to write asynchronous code using the usual declarative syntax!

Switch to async / await


So, how to start using async / await in your projects? If you are already working with promises, then you are ready to move to a new technology. Any function that returns a promis can be called using the await keyword, which causes it to return the result of the promise resolution. However, if you are going to switch to async / await from callbacks, you will first need to convert them to promises.

â–ŤMoving async / await from promises


If you are one of those who are at the forefront of developers who have accepted promises, and in your code, to implement asynchronous logic, use .then() chains, switching to async / await will not be difficult: you just need to rewrite each .then() construct .then() using await .

In addition, the .catch() block must be replaced with standard try / catch blocks. As you can see, finally we can use the same approach for error handling in synchronous and asynchronous contexts!

It is also important to note that the await keyword cannot be used at the top level of modules . It must be used inside functions declared with the async .

 let hn = require('@datafire/hacker_news').create(); //    : Promise.resolve() .then(_ => hn.getStories({storyType: 'top'})) .then(storyIDs => hn.getItem({itemID: storyIDs[0])) .then(topStory => console.log(topStory)) .catch(e => console.error(e)) //    async / await: (async () => { try {   let storyIDs = await hn.getStories({storyType: 'top'});   let topStory = await hn.getItem({itemID: storyIDs[0]});   console.log(topStory); }  catch (e) {   console.error(e); } })(); 

â–Ť Transfer to async / await from callbacks


If your code still uses callback functions, the best way to switch to async / await is to pre-convert callbacks into promises. Then, using the above method, the code using promises is rewritten using async / await . You can read about how to convert callbacks into promises here .

Patterns and pitfalls


Of course, new technologies are always new problems. Here are some useful patterns and sample errors you may encounter when translating your code into async / await .

â–Ť Cycles


Ever since I started writing JS, passing functions as arguments to other functions was one of my favorite features. Of course, callbacks are a mess, but I, for example, preferred to use Array.forEach instead of the usual for loop:

 const BEATLES = ['john', 'paul', 'george', 'ringo']; //   for: for (let i = 0; i < BEATLES.length; ++i) { console.log(BEATLES[i]); } //  Array.forEach: BEATLES.forEach(beatle => console.log(beatle)) 

However, using await the Array.forEach method will not work properly, since it is designed to perform synchronous operations:

 let hn = require('@datafire/hacker_news').create(); (async () => { let storyIDs = await hn.getStories({storyType: 'top'}); storyIDs.forEach(async itemID => {   let details = await hn.getItem({itemID});   console.log(details); }); console.log('done!'); // !      ,    getItem()  . })(); 

In this example, forEach runs a bunch of simultaneous asynchronous calls to getItem() and returns immediately, without waiting for the results, so the first thing that is displayed on the screen is the string “done!”.

If you need to wait for the results of asynchronous operations, it means that you need either a normal for loop (which will perform the operations sequentially) or the Promise.all construction (it will perform the operations in parallel):

 let hn = require('@datafire/hacker_news').create(); (async () => { let storyIDs = await hn.getStories({storyType: 'top'}); //   for (  ) for (let i = 0; i < storyIDs.length; ++i) {   let details = await hn.getItem({itemID: storyIDs[i]});   console.log(details); } //  Promise.all (  ) let detailSet = await Promise.all(storyIDs.map(itemID => hn.getItem({itemID}))); detailSet.forEach(console.log); })(); 

â–ŤOptimization


When using async / await you no longer need to think about what you are writing asynchronous code. This is fine, but here lies the most dangerous trap of new technology. The fact is that with this approach, you can forget about the little things that can have a huge impact on performance.

Consider an example. Suppose we want to get information about two Hacker News users and compare their karma. Here is the usual implementation:

 let hn = require('@datafire/hacker_news').create(); (async () => { let user1 = await hn.getUser({username: 'sama'}); let user2 = await hn.getUser({username: 'pg'}); let [more, less] = [user1, user2].sort((a, b) => b.karma - a.karma); console.log(`${more.id} has more karma (${more.karma}) than ${less.id} (${less.karma})`); })(); 

The code is quite working, but the second getUser() call will not be executed until the first one is completed. The calls are independent, they can be executed in parallel. Therefore, below is a better solution:

 let hn = require('@datafire/hacker_news').create(); (async () => { let users = await Promise.all([   hn.getUser({username: 'sama'}),   hn.getUser({username: 'pg'}), ]); let [more, less] = users.sort((a, b) => b.karma - a.karma); console.log(`${more.id} has more karma (${more.karma}) than ${less.id} (${less.karma})`); })(); 

It is worth noting that before using this method, it is necessary to make sure that the desired can be achieved by parallel execution of commands. In many cases, asynchronous operations must be performed sequentially.

Results


Hopefully, I was able to show you what great innovations the async / await construct has contributed to the development of asynchronous JavaScript code. The ability to describe asynchronous constructions using the same syntax as synchronous is the standard of modern programming. And the fact that now the same opportunity is available in JavaScript is a huge step forward for everyone who writes in this language.

Dear readers! We know from a survey from a previous publication that many of you use async / await. Therefore, please share your experience.

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


All Articles