Parsing Async / Await in JavaScript with Examples

Original author: Arfat Salman
  • Transfer


The author of the article parses Async / Await in JavaScript using examples. In general, Async / Await is a convenient way to write asynchronous code. Prior to this opportunity, similar code was written using callbacks and promises. The author of the original article reveals the benefits of Async / Await by examining various examples.

We remind you: for all readers of “Habr” - a discount of 10,000 rubles when registering for any Skillbox course using the “Habr” promo code.

Skillbox recommends: The Java Developer Online Education Course .

Callback


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

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

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

Problems arise when you need to perform several asynchronous operations at once. Let's imagine this scenario: a request is made to the Arfat user database, you need to read its profile_img_url field and download a picture from someserver.com server.
After downloading, convert the image to another format, for example, from PNG to JPEG. If the conversion was successful, an email is sent to the user's mail. Further, information about the event is entered in the transformations.log file 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 a Callback Hell or Pyramid of Doom.

The disadvantages of this method are obvious:

  • This code is hard to read.
  • It is also difficult to handle errors in it, which often leads to a deterioration in the quality of the code.

In order to solve this problem, promises were added to 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, from top to bottom, and not from left to right. Nevertheless, promises also have their problems:

  • Need to add a large amount of .then.
  • Instead of try / catch, .catch is used to handle all errors.
  • Working with several promises within one cycle is far from always convenient; in some cases, they complicate the code.

Here is a task that will show the meaning of the last paragraph.

Suppose there is a for loop that prints a sequence of numbers from 0 to 10 with 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 take 2 seconds, zero should be output first, and then the unit output countdown 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) has simplified the task of working with promises. I note that async functions work on top of promises. These functions do not represent qualitatively different concepts. Async functions were conceived as an alternative to code that uses promises.

Async / Await makes it possible 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

In a typical situation, it consists of two keywords: async and await. The first word makes the function asynchronous. These functions allow await. 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 the 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.

The semantics and rules for executing an

Async function are, in principle, 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 the 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 the promise is made "manually". An asynchronous function always wraps itself in a new promise.

In the event that the return value is a primitive, the async function returns a value, wrapping it in a promise. In the event that the return value is the object of the 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 the asynchronous function?

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

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

Async functions on output always give promises, regardless of what is returned.

Asynchronous functions are paused on every await.

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

// 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);

Here is a description of how the fn function works.

  • After calling it, the first line is converted from const a = await 9; in const a = await Promise.resolve (9) ;.
  • After using Await, the execution of the function is suspended until it receives its value (in the current situation, it is 9).
  • delayAndGetRandom (1000) pauses the execution of the fn function until it finishes itself (after 1 second). This is actually stopping the fn function for 1 second.
  • delayAndGetRandom (1000) through resolve returns a random value, which is then assigned to the variable b.
  • Well, the case of variable c is similar to the case of variable a. After that, everything stops for a second, but now delayAndGetRandom (1000) returns nothing, since this is not required.
  • As a result, the values ​​are calculated by the formula a + b * c. The result is wrapped in a promise using Promise.resolve and returned by the function.

These pauses may resemble generators in ES6, but there are reasons for this .

We solve the problem


Well, now let's look at 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 decision with where the promises were used, the similarities will become apparent. Nevertheless, the version with Async / Await greatly simplifies all syntactic difficulties. In this case, there are not a lot 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);
    });
  });
};

And here is a solution using async functions.

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

Error Handling

Unhandled errors are wrapped in a rejected promise. However, in async functions, you can use the 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 succeeds (“perfect number”) or fails with an error (“Sorry, number too big”).

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

Since canRejectOrReturn is expected to execute in the example above, its own unsuccessful termination will entail the execution of the 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, as try / catch will handle the foo function 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 from foo canRejectOrReturn is returned. Foo in this case either completes with a perfect number or returns an Error (“Sorry, number too big”) error. The catch block will never be executed.

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

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

But what happens if you use await and return together:

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

In the code above, foo succeeds with both perfect number and error caught. There will be no failures. But foo will end with canRejectOrReturn, and not with undefined. Let's make sure of 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 may result in errors.

Forgotten await

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

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

In the code, as you can see, there is neither await nor return. Therefore, foo always exits with undefined without a delay of 1 second. But the promise will be fulfilled. If it gives an error or a redject, then UnhandledPromiseRejectionWarning will be called.

Async functions in callbacks

Async functions are often used in .map or .filter as callbacks. An example is the fetchPublicReposCount (username) function, which returns the number of repositories open on GitHub. Let's say there are three users whose metrics 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, execute:

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

You should pay attention to Await in the .map callback. Here counts is an array of promises, well .map is an anonymous callback for each specified user.

Excessively consistent use of await

As an example, take the following 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 repo number is placed in the count variable, then this number is added to the counts array. The problem with the code is that until the first user data arrives from the server, all subsequent users will be in standby mode. Thus, in a single moment, only one user is processed.

If, for example, it takes about 300 ms to process one user, then for all users this is already a second, the time spent linearly depends on the number of users. But since getting the number of repos does not depend on each other, the processes can be parallelized. This requires work 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 input receives an array of promises with the return of the promise. The last one after completion of all promises in the array or at the first redject 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 p-map.

Conclusion


Async features are becoming increasingly important to development. Well, for adaptive use of async-functions it is worth using Async Iterators . The JavaScript developer should be well versed in this.

Skillbox recommends:


Also popular now: