Original text: blog. Bitsrc. IO/understandi… Translated by Arfat Salman: The front end is white

First we’ll talk about callback functions. Callback functions are nothing special, just functions that will be executed at some point in the future. Because of the asynchronous nature of JavScript, callbacks are required in many places where results are not immediately available

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

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

Problems arise when we want to perform multiple asynchronous operations. Imagine the following scenario (all operations are asynchronous) :

  • We query the user in the databaseArfat. We readprofile_img_urlAnd from thesomeServer.comGet the image.
  • Once the image is taken, we convert it to another format, such as PNG to JPEG.
  • If the conversion is successful, we send an E-mail to the user.
  • intransformations.logRecord the task with a time stamp

The code looks like this:

The callback hell

Note the hierarchy of the nested end of the callback function}), which has been nicknamed callback hell or callback pyramid. The disadvantage is that –

  • Bad code readability
  • Error handling is complex and often results in bad code.

To address these issues, JavaScript makes promises. Now we can use the chain structure instead of the nested structure of callback functions. Here’s an example

The use of promise

The process is now structured top-down rather than left to right, which is a plus. But promise still has some drawbacks

  • At the end of each.thenWe still have to deal with callbacks
  • Compared to using normaltry/catchWe’re going to use.catch()Handling errors
  • Multiple processes are processed sequentially in a looppromisesIt’s very painful. It’s not intuitive

Let’s demonstrate the last disadvantage:

challenge

Suppose we have a for loop that prints 0 to 10 at random intervals (0 to n seconds). We need to print them out in order 0 to 10 using promises. For example, if 0 takes 6 seconds to print and 1 takes 2 seconds to print, then 1 should wait for 0 to finish printing before printing, and so on.

Do not use async/await or.sort, we will fix this later

Async function

ES2017(ES8) introduces async functions that make it easy to apply promises

  • It is important to note that:asyncThe use of functions is based onpromise
  • They are not completely different concepts
  • Can be considered as a kind of basispromiseAn alternative to asynchronous code
  • async/awaitYou can avoid usingpromiseChain calls
  • The code executes asynchronously and looks synchronous

Therefore, to understand async/await you must first understand promises

grammar

Async /await contains two keywords async and await. Async is used to make functions run asynchronously. Async allows us to use await in functions, otherwise it is a syntax error to use await anywhere.

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

Notice that the async keyword precedes the function declaration in the function declaration. In the arrow function, the async keyword is placed between = and parentheses.

Async functions can also be used as methods on objects or in class declarations.

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

Note: class constructor andgetters/settersAsync functions cannot be used.

Semantics and evaluation criteria

Async is a normal JavaScript function with the following differences

Async functions always return a Promise object.

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

The function fn returns ‘hello’, and since we are using the async keyword, ‘hello’ is wrapped as a Promise object (implemented via the Promise constructor)

This is an alternative implementation that does not use async

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

In the code above, we returned a Promise object manually without using the async keyword

More precisely, the return value of the async function is wrapped in promise.resolve.

Resolve returns a promised value if the return value is the original value, or if the return value is a Promise object, the Promise object is returned

// in case of primitive values
const p = Promise.resolve('hello')
p instanceof Promise; 
// true
// p is returned as is it
Promise.resolve(p) === p; 
// true
Copy the code

What if an async function throws an error?

For instance,

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

If the error is not caught, the foo() function returns a promise with the status Rejected. Unlike promise.resolve, promise.reject wraps the error and returns. See error Handling later for details.

The end result is that whatever result you return, you will get a promise from the Async function.

Async functions pause when encountering await < expression >

Await applies to an expression. When the expression is a PROMISE, the async function suspends execution until the Promise state changes to Resolved. When the expression is a non-PROMISE value, promise.resolve is used to convert it to a PROMISE, and then the state changes to Resolved.

// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) = > {
  return new Promise(resolve= > setTimeout(
    (a)= > {
      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);
Copy the code

Let’s look at the FN function line by line

  • The first line of code when the function executesconst a = await 9, the inside will be resolved toconst a = await Promise.resolve(9)
  • Because it usesawait, so function execution is paused until the variableaGet a value, which in the promise will resolve to 9
  • delayAndGetRandom(1000)Can makefnThe function pauses until 1 second laterdelayAndGetRandomResolve, therefore,fnExecution of the function is effectively paused for 1 second
  • In addition,delayAndGetRandomReturns a random number. Whatever is passed in resolve is assigned to the variable, rightb.
  • Again, variablescThe value is 5, then useawait delayAndGetRandom(1000)There was another delay of one second. We don’t use it in this line of codePromise.resolveThe return value.
  • And finally, we calculatea + b * cResult, usePromise.resolvePackage and return

Note: If the pause and resume of functions here reminds you of ES6 generators, it’s because Generator has many advantages

The solution

Let’s use async/await to solve the hypothetical problem raised at the beginning of the article:

Use the async/await

We define an async function finishMyTask and await results from queryDatabase, sendEmail, logTaskInFile with await

If we compare the async/await solution with the one using Promise, we see a similar amount of code. But async/await makes code look simpler without memorizing multiple layers of callback functions and.then /.catch.

Now let’s solve the problem of printing numbers. There are two solutions

const wait = (i, ms) = > new Promise(resolve= > setTimeout((a)= > resolve(i), ms));

// Implementation One (Using for-loop)
const printNumbers = (a)= > 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 = (a)= > {
  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);
    });
  });
};
Copy the code

It’s even simpler if you use async

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

Error handling

As we learned in the syntax section, an uncaught Error() is wrapped in a Rejected Promise, but we can use try-catch to handle errors synchronously in async functions. Let’s start with this useful function

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

CanRejectOrReturn () is an asynchronous function, either resolve ‘perfect number’ or reject Error(‘Sorry, number too big’)

Look at the code below

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

Since we’re waiting for canRejectOrReturn to execute, its rejection is translated into an error throw, and the catch is executed, which means foo’s resolve is either undefined (because we didn’t return a value in the try), Either resolve ‘error caught’. Foo is never rejected because we use a try-catch in foo.

Another example

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

Pay attention to. Instead of waiting for canRejectOrReturn, foo will either resolve ‘perfect number’ or reject Error(‘Sorry, number too big’), The catch statement is not executed

Since we return the promise object returned by canRejectOrReturn, foo’s final state is determined by the state of canRejectOrReturn, You can break the return canRejectOrReturn() into two lines to get a clearer idea, noting that the first line has no await

try {
    const promise = canRejectOrReturn();
    return promise;
}
Copy the code

Let’s look at using return and await together

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

In this case foo resolve ‘perfect number’ or resolve ‘error caught’ with no rejection like the above example with await only Here we resolve the value returned by canRejectOrReturn, not undefined

We can also split return await canRejectOrReturn()

try {
    const value  = await canRejectOrReturn();
    return value;
}
// ...
Copy the code

Common mistakes and pitfalls

Due to the intricacies of the operation between Promise and async/await. There may be some hidden errors, let’s take a look at –

No use of await

Sometimes we forget to use await before promises, or forget to return

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

Note that foo will always resolve undefined if we do not use await or return, and will not wait a second, but the promise in canRejectOrReturn() does get executed. If there is a side effect, also can produce, if throw an error or reject, UnhandledPromiseRejectionWarning can produce

Use async functions in callbacks

We often use async functions as callbacks in.map and.filter, assuming we have a function fetchPublicReposCount(username) that gets the number of public repositories owned by a Github user. We want to get the number of open repositories for three different users, so let’s look at the code —

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'];
}
Copy the code

We want to get the number of warehouses for [‘ArfatSalman’, ‘Octocat ‘,’ Norvig ‘] and do this:

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

Note that async in the.map callback, we want the COUNTS variable to contain the number of storehouses. However, as we saw earlier, async returns a Promise object, so COUNTS is actually a Promises array, This array contains the promise returned each time the function is called to get the number of user repositories,

Excessive use of await sequentially

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

We manually retrieved each count and saved them in the COUNTS array. The problem with the program is that the count of the second user can be fetched only after the first user’s count is fetched. Only one warehouse can be retrieved at a time.

If a FETCH operation takes 300 ms, the fetchAllCounts function takes about 900 ms. Thus, the program time increases linearly as the number of users increases. Because there is no dependency between getting the number of repositories exposed by different users, we can parallel the operations.

We can acquire users simultaneously rather than sequentially. We’ll use.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);
}
Copy the code

Promise.all takes an array of Promise objects as input and returns a Promise object as output. When all promise objects are switched to Resolved, the return value is an array of resolved results, and when it fails, the first rejected state is returned. As long as one promise object is rejected, Promise.all returns the value corresponding to the first Promise object rejected. However, running all promises at the same time may not work. If you want to make promises in bulk. Refer to p-Map for quantity-controlled concurrent operations.

conclusion

Async functions are very important. With the introduction of Async Iterators, Async functions will be more and more widely used. It is essential for modern JavaScript developers to master and understand async functions. I hope this article has been helpful to you