⬆️ ⬇️

Parse Async / Await in JavaScript with examples





The author of the article examines Async / Await in JavaScript. In general, Async / Await is a convenient way to write asynchronous code. Before the appearance of this feature, a similar code was written using callbacks and promises. The author of the original article reveals the advantages of Async / Await, examining various examples.



We remind: for all readers of "Habr" - a discount of 10,000 rubles when writing to any Skillbox course on the promotional code "Habr".



Skillbox recommends: Educational online course "Java-developer" .


Callback



Callback is a function whose call is postponed indefinitely. Previously, callbacks were used in those areas of the code where the result could not be obtained immediately.

')

Here is an example of an asynchronous file reading on Node.js:



fs.readFile(__filename, 'utf-8', (err, data) => { if (err) { throw err; } console.log(data); }); 


Problems arise at the moment when you need to perform several asynchronous operations at once. Let's imagine the following scenario: a request is made to the Arfat user database, you need to read its profile_img_url field and upload a picture from the server someserver.com.

After downloading, we convert the image to another format, for example, from PNG to JPEG. If the conversion is successful, an email is sent to the user's mail. Further information about the event is recorded in the file transformations.log with the date.







It is worth paying attention to the imposition of callbacks and a large number}) in the final part of the code. This is called Callback Hell or Pyramid of Doom.



The disadvantages of this method are obvious:





In order to solve this problem, promises have been added in JavaScript. They allow you to replace the deep nesting of callbacks with the word .then.







The positive point of the promises was that with them the code is read much better, and from top to bottom, and not from left to right. Nevertheless, promises also have their own problems:





Here is the task that will show the value of the last item.



Suppose there is a for loop that outputs a sequence of numbers from 0 to 10 at a random interval (0 – n seconds). Using promises, you need to change this cycle so that the numbers are displayed in the sequence from 0 to 10. So, if the zero output takes 6 seconds and the units 2 seconds, zero should be displayed first, and then the unit output will begin.



And of course, to solve this problem, we do not use Async / Await or .sort. An example of a solution is at the end.



Async functions



Adding async functions to ES2017 (ES8) simplified the task of working with promises. I note that the async functions work "on top" of promises. These functions are not qualitatively different concepts. Async functions were thought of as an alternative to code that uses promises.



Async / Await allows you to organize work with asynchronous code in a synchronous style.



Thus, knowledge of promises makes it easier to understand the principles of Async / Await.



Syntax



Normally, it consists of two keywords: async and await. The first word and turns the function into asynchronous. In such functions, the use of await is permitted. In any other case, using this function will cause an error.



 // With function declaration async function myFn() { // await ... } // With arrow function const myFn = async () => { // await ... } function myFn() { // await fn(); (Syntax Error since no async) } 


Async is inserted at the very beginning of the function declaration, and in the case of using an arrow function, between the “=” sign and the brackets.



These functions can be placed in an object as methods or used in a class declaration.



 // As an object's method const obj = { async getName() { return fetch('https://www.example.com'); } } // In a class class Obj { async getResource() { return fetch('https://www.example.com'); } } 


NB! It is worth remembering that class constructors and getters / setters cannot be asynchronous.



Semantics and execution rules



Async functions, in principle, are similar to standard JS functions, but there are exceptions.



So, async functions always return promises:



 async function fn() { return 'hello'; } fn().then(console.log) // hello 


In particular, fn returns the string hello. Well, since this is an asynchronous function, the string value is wrapped in a promise using a constructor.



Here is an alternative design without Async:



 function fn() { return Promise.resolve('hello'); } fn().then(console.log); // hello 


In this case, the return of promis made "manually." The asynchronous function is always wrapped in a new promise.



In that case, if the return value is primitive, the async function returns the value, wrapping it in promise. In the event that the returned value is the object of promise, its solution is returned in the new promise.



 const p = Promise.resolve('hello') p instanceof Promise; // true Promise.resolve(p) === p; // true 


But what happens if an error occurs inside an asynchronous function?



 async function foo() { throw Error('bar'); } foo().catch(console.log); 


If it is not processed, foo () will return a promise with a reject. In this situation, the Promise.reject containing the error will be returned instead of the Promise.resolve.



Async functions at the output always give a promise, regardless of what is returned.



Asynchronous functions are suspended with each await.



Await affects expressions. So, if the expression is promis, the async function is suspended until the promise is executed. In the event that the expression is not a promise, it is converted into promise through Promise.resolve and then completed.



 // utility function to cause delay // and get random value const delayAndGetRandom = (ms) => { return new Promise(resolve => setTimeout( () => { const val = Math.trunc(Math.random() * 100); resolve(val); }, ms )); }; async function fn() { const a = await 9; const b = await delayAndGetRandom(1000); const c = await 5; await delayAndGetRandom(1000); return a + b * c; } // Execute fn fn().then(console.log); 


And here is a description of how the fn-function works.





These pauses may resemble generators in ES6, but this has its own reasons .



Solve the problem



Well, now let's consider the solution to the problem that was mentioned above.







The finishMyTask function uses Await to wait for the results of operations such as queryDatabase, sendEmail, logTaskInFile, and others. If we compare this solution with where the promises were used, the similarity will become apparent. Nevertheless, the version with Async / Await rather simplifies all syntactic complexities. In this case, there are not a large number of callbacks and chains like .then / .catch.



Here is a solution with the output of numbers, there are two options.



 const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms)); // Implementation One (Using for-loop) const printNumbers = () => new Promise((resolve) => { let pr = Promise.resolve(0); for (let i = 1; i <= 10; i += 1) { pr = pr.then((val) => { console.log(val); return wait(i, Math.random() * 1000); }); } resolve(pr); }); // Implementation Two (Using Recursion) const printNumbersRecursive = () => { return Promise.resolve(0).then(function processNextPromise(i) { if (i === 10) { return undefined; } return wait(i, Math.random() * 1000).then((val) => { console.log(val); return processNextPromise(i + 1); }); }); }; 


But the solution is using async functions.



 async function printNumbersUsingAsync() { for (let i = 0; i < 10; i++) { await wait(i, Math.random() * 1000); console.log(i); } } 


Error processing



Raw errors are wrapped in rejected promises. However, in async functions, you can use a try / catch construct to perform synchronous error handling.



 async function canRejectOrReturn() { // wait one second await new Promise(res => setTimeout(res, 1000)); // Reject with ~50% probability if (Math.random() > 0.5) { throw new Error('Sorry, number too big.') } return 'perfect number'; } 


canRejectOrReturn () is an asynchronous function that either successfully executes (“perfect number”) or fails with an error (“Sorry, number too big”).



 async function foo() { try { await canRejectOrReturn(); } catch (e) { return 'error caught'; } } 


Since the example above is expected to execute canRejectOrReturn, then its own failure will result in the execution of a catch block. As a result, the foo function will end either with undefined (when nothing is returned in the try block) or with error caught. As a result, this function will not fail because the try / catch will handle the function foo itself.



Here is another example:



 async function foo() { try { return canRejectOrReturn(); } catch (e) { return 'error caught'; } } 


It is worth paying attention to the fact that in the example foo returns canRejectOrReturn. Foo in this case completes either the perfect number, or an Error (“Sorry, number too big”) error is returned. The catch block will never be executed.



The problem is that foo returns the promise sent from canRejectOrReturn. Therefore, the solution of the foo function becomes the solution for canRejectOrReturn. In this case, the code will consist of only two lines:



 try { const promise = canRejectOrReturn(); return promise; } 


This is what will happen if you use await and return together:



 async function foo() { try { return await canRejectOrReturn(); } catch (e) { return 'error caught'; } } 


In the code above, foo will successfully complete with both the perfect number and error caught. There will be no failures. But foo will end with canRejectOrReturn, not with undefined. Let's verify this by removing the return await canRejectOrReturn () line:



 try { const value = await canRejectOrReturn(); return value; } // … 


Common mistakes and pitfalls



In some cases, using Async / Await can lead to errors.



Forgotten await



This happens quite often - the await keyword is forgotten before the promise:



 async function foo() { try { canRejectOrReturn(); } catch (e) { return 'caught'; } } 


In the code, as you can see, there is no await or return. Therefore, foo always ends with undefined without a 1 second delay. But the promise will be done. If it gives an error or a rejection, then in this case UnhandledPromiseRejectionWarning will be called.



Async functions in callbacks



Async functions are often used in .map or .filter as callbacks. An example is the function fetchPublicReposCount (username), which returns the number of repositories open on GitHub. Suppose there are three users whose indicators we need. Here is the code for this task:



 const url = 'https://api.github.com/users'; // Utility fn to fetch repo counts const fetchPublicReposCount = async (username) => { const response = await fetch(`${url}/${username}`); const json = await response.json(); return json['public_repos']; } 


We need accounts ArfatSalman, octocat, norvig. In this case we execute:



 const users = [ 'ArfatSalman', 'octocat', 'norvig' ]; const counts = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); 


Pay attention to Await in the callback .map. Here counts is an array of promises, but .map is an anonymous callback for each specified user.



Overuse of await



As an example, take this code:



 async function fetchAllCounts(users) { const counts = []; for (let i = 0; i < users.length; i++) { const username = users[i]; const count = await fetchPublicReposCount(username); counts.push(count); } return counts; } 


Here the number of the repo is placed in the count variable, then this number is added to the counts array. The problem with the code is that until the data of the first user comes from the server, all subsequent users will be in standby mode. Thus, only one user is processed at a single moment.



If, for example, it takes about 300 ms to process one user, then for all users it is already a second, the time spent linearly depends on the number of users. But once the receipt of the number of repos does not depend on each other, the processes can be parallelized. This requires working with .map and Promise.all:



 async function fetchAllCounts(users) { const promises = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); return Promise.all(promises); } 


Promise.all at the entrance receives an array of promises with the return of promise. After the completion of all promises in the array or at the first rejection, the last one is completed. It may happen that all of them do not start at the same time, - in order to ensure simultaneous launch, you can use a p-map.



Conclusion



Async functions are becoming increasingly important for development. Well, for adaptive use of async-functions, you should use Async Iterators . A javascript developer should be good at this.



Skillbox recommends:



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



All Articles