async function

Meaning

The ES2017 standard introduces the async function to make asynchronous operations more convenient.

What is an async function? In a nutshell, it's syntactic sugar for the Generator function.

The previous section has a Generator function that reads two files in turn.

const fs = require("fs");

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile("/etc/fstab");
  const f2 = yield readFile("/etc/shells");
  console.log(f1.toString());
  console.log(f2.toString());
};

The function gen in the above code can be written as an async function, which is the following.

const asyncReadFile = async function () {
  const f1 = await readFile("/etc/fstab");
  const f2 = await readFile("/etc/shells");
  console.log(f1.toString());
  console.log(f2.toString());
};

A quick comparison reveals that the async function replaces the asterisk (*) in the Generator function with async and the yield with await, and that's it.

The async function is an improvement on the Generator function, reflected in the following four points.

(1) Built-in executor.

The execution of the Generator function must rely on the executor, which is why there is the co module, and the async function comes with its own executor. In other words, the execution of the async function is exactly the same as a normal function, just one line.

asyncReadFile();

The code above calls the asyncReadFile function, which then executes automatically and outputs the final result. This is completely unlike the Generator function, where you need to call the next method, or use the co module, to actually execute and get the final result.

(2) Better semantics.

The semantics of async and await are much clearer than asterisks and yield. async indicates that there is an asynchronous operation in the function, and await indicates that the expression immediately following it needs to wait for the result.

(3) Broader applicability.

The co module convention is that the yield command can only be followed by a Thunk function or a Promise object, whereas the await command of the async function can be followed by a Promise object and a value of the original type (numeric, string and boolean, but then it is automatically converted to a resolved Promise object).

(4) The return value is a Promise.

The return value of the async function is a Promise object, which is much more convenient than the return value of the Generator function which is an Iterator object. You can use the then method to specify the next action.

Further, the async function can be thought of as multiple asynchronous operations wrapped into a single Promise object, and the await command is the syntactic sugar for the internal then command.

Basic usage

The async function returns a Promise object to which a callback function can be added using the then method. When the function is executed, it will return as soon as it encounters await, wait until the asynchronous operation is complete, and then proceed to execute the subsequent statements within the function.

Here is an example.

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName("goog").then(function (result) {
  console.log(result);
});

The above code is a function that gets a stock quote. The async keyword in front of the function indicates that there is an asynchronous operation inside the function. When the function is called, a Promise object is immediately returned.

Here is another example that specifies how many milliseconds to output a value.

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint("hello world", 50);

The above code specifies that hello world will be output after 50 milliseconds.

Since the async function returns a Promise object, it can be used as an argument to the await command. So, the above example could also be written in the following form.

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint("hello world", 50);

The async function can be used in a variety of ways.

// function declaration
async function foo() {}

// function expressions
const foo = async function () {};

// Methods for objects
let obj = { async foo() {} }
obj.foo().then(...)

// methods of a class
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(...);

// arrow function
const foo = async () => {};

Syntax

The syntax rules for the async function are generally simple, the hard part is the error handling mechanism.

Returning a Promise object

The async function returns a Promise object.

The value returned by the return statement inside the async function becomes an argument to the then method callback function.

async function f() {
  return "hello world";
}

f().then((v) => console.log(v));
// "hello world"

In the above code, the value returned by the return command inside the function f is picked up by the then method callback function.

Throwing an error inside the async function causes the returned Promise object to change to the reject state. The thrown error object is picked up by the catch method callback function.

async function f() {
  throw new Error("There was an error");
}

f().then(
  (v) => console.log("resolve", v),
  (e) => console.log("reject", e)
);
//reject Error: An error occurred

State changes of Promise objects

The Promise object returned by the async function must wait until all the Promise objects following the internal await command have been executed before the state change occurs, unless a return statement is encountered or an error is thrown. That is, the callback function specified by the then method will not be executed until the asynchronous operation inside the async function has been executed.

Here is an example.

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle("https://tc39.github.io/ecma262/").then(console.log);
// "ECMAScript 2017 Language Specification"

In the above code, the function getTitle has three internal operations: crawling the page, taking out the text, and matching the page title. Only when all three operations are completed will the console.log inside the then method be executed.

The await command

Normally, the await command is followed by a Promise object and returns the result of that object. If it is not a Promise object, the corresponding value is returned directly.

async function f() {
  // is equivalent to
  // return 123;
  return await 123;
}

f().then((v) => console.log(v));
// 123

In the above code, the argument to the await command is the value 123, which in this case is equivalent to return 123.

Alternatively, if the await command is followed by a thenable object (i.e. an object that defines a then method), then await will equate it to a Promise object.

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(() => resolve(Date.now() - startTime), this.timeout);
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();
// 1000

In the above code, the await command is followed by an instance of a Sleep object. This instance is not a Promise object, but because the then method is defined, await treats it as a Promise.

This example also shows how to implement a sleep effect. JavaScript has never had a syntax for sleep, but the await command allows the program to pause for a specified amount of time. A simplified implementation of sleep is given below.

function sleep(interval) {
  return new Promise((resolve) => {
    setTimeout(resolve, interval);
  });
}

// Usage
async function one2FiveInAsync() {
  for (let i = 1; i <= 5; i++) {
    console.log(i);
    await sleep(1000);
  }
}

one2FiveInAsync();

If the Promise object behind the await command changes to the reject state, the arguments to reject are received by the callback function of the catch method.

async function f() {
  await Promise.reject("There was an error");
}

f()
  .then((v) => console.log(v))
  .catch((e) => console.log(e));
// Something went wrong

Note that in the above code, the await statement is not preceded by return, but the argument to the reject method is still passed into the callback function of the catch method. Here the effect is the same if return is added before await.

Any await statement followed by a Promise object becomes reject, and the entire async function will be interrupted.

async function f() {
  await Promise.reject("There was an error");
  await Promise.resolve("hello world"); // will not execute
}

In the above code, the second await statement is not executed because the state of the first await statement has changed to reject.

Sometimes it is desirable not to interrupt a later asynchronous operation even if the previous one fails. In this case, the first await can be placed inside a try.... .catch structure, so that the second await will be executed regardless of whether the asynchronous operation succeeds or not.

async function f() {
  try {
    await Promise.reject("There was an error");
  } catch (e) {}
  return await Promise.resolve("hello world");
}

f().then((v) => console.log(v));
// hello world

Another approach is to follow the Promise object with await followed by a catch method to handle any errors that may have occurred earlier.

async function f() {
  await Promise.reject("There was an error").catch((e) => console.log(e));
  return await Promise.resolve("hello world");
}

f().then((v) => console.log(v));
// There was an error
// hello world

Error handling

If the asynchronous operation following await fails, then it is equivalent to the Promise object returned by the async function being rejected.

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error("There was an error");
  });
}

f()
  .then((v) => console.log(v))
  .catch((e) => console.log(e));
// Error: there was an error

In the above code, after the execution of the async function f, the Promise object behind await will throw an error object, causing the callback function of the catch method to be called, and its argument is the error object thrown. For more details on the execution mechanism, see "How the async function is implemented" later in this article.

The way to prevent errors is to put them in the try.... .catch block.

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error("There was an error");
    });
  } catch (e) {}
  return await "hello world";
}

If there are multiple await commands, they can be unified in a try. .catch structure.

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log("Final: ", val3);
  } catch (err) {
    console.error(err);
  }
}

The following example uses the try... .catch construct to implement multiple repeat attempts.

const superagent = require("superagent");
const NUM_RETRIES = 3;

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get("http://google.com/this-throws-an-error");
      break;
    } catch (err) {}
  }
  console.log(i); // 3
}

test();

In the above code, if the await operation succeeds, it will exit the loop using the break statement; if it fails, it will be caught by the catch statement and then move on to the next round of loops.

Points to note on use

The first point, as already mentioned, is that the Promise object following the await command may run as rejected, so it is best to place the await command in the try.... .catch block.

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// Another way to write it

async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err) {
    console.log(err);
  });
}

The second point is that multiple await commands followed by asynchronous operations are best left to trigger simultaneously if there is no relay relationship.

let foo = await getFoo();
let bar = await getBar();

In the above code, getFoo and getBar are two separate asynchronous operations (i.e. not dependent on each other) that are written as a relay relationship. This is more time consuming because getBar is only executed after getFoo completes, and it is perfectly possible to have them trigger at the same time.

// Write one
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// Write two
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

In the above two ways of writing, getFoo and getBar are both triggered at the same time, which will shorten the execution time of the program.

The third point is that the await command can only be used in the async function, if it is used in a normal function, it will report an error.

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // error is reported
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

The above code will report an error because await is used in a normal function. However, if you change the argument of the forEach method to an async function, you also have a problem.

function dbFuc(db) {
  // no async needed here
  let docs = [{}, {}, {}];

  // may get an error result
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

The reason the above code may not work properly is that the three db.post() operations will then be executed concurrently, i.e. at the same time, rather than successively. The correct way to write it is to use a for loop.

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

Another way is to use the ``reduce()` method of the array.

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  await docs.reduce(async (_, doc) => {
    await _;
    await db.post(doc);
  }, undefined);
}

In the above example, the first argument to the reduce() method is the async function, resulting in the first argument to that function being the Promise object returned from the previous operation, so you must use await to wait for it to finish its operation. In addition, the reduce() method returns the result of the async function of the last member of the docs array, which is also a Promise object, so it must be preceded by await as well.

The reason there is no return statement inside the reduce() argument function above is that the main purpose of the function is the db.post() operation, not the return value. Also, the async function always returns a Promise object with or without the return statement, so the return here is unnecessary.

If you do want multiple requests to be executed concurrently, you can use the Promise.all method. When all three requests will resolved, the following two ways of writing will have the same effect.

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

The fourth point is that the async function can preserve the run stack.

const a = () => {
  b().then(() => c());
};

In the above code, the function a runs an asynchronous task b() internally. While b() is running, the function a() does not break, but continues to execute. By the time b() has finished running, it is likely that a() will have finished running long ago and the context in which b() is running will have disappeared. If b() or c() reports an error, the error stack will not include a().

Now change this example to the async function.

const a = async () => {
  await b();
  c();
};

In the above code, b() runs while a() is suspended and the contextual environment is preserved. Once b() or c() reports an error, the error stack will include a().

How the async function is implemented

The async function is an implementation of the Generator function and the auto-executor, wrapped in a single function.

async function fn(args) {
  // ...
}

// is equivalent to

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

All async functions can be written as the second form above, where the spawn function is the auto-executor.

The implementation of the spawn function is given below, and is basically a rehash of the previous auto-executor.

function spawn(genF) {
  return new Promise(function (resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch (e) {
        return reject(e);
      }
      if (next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v);
          });
        },
        function (e) {
          step(function () {
            return gen.throw(e);
          });
        }
      );
    }
    step(function () {
      return gen.next(undefined);
    });
  });
}

Comparison with other asynchronous processing methods

Let's look at how the async function compares to the Promise and Generator functions with an example.

Suppose a DOM element has a series of animations deployed on it, with the first one ending before the next one can start. If one of the animations goes wrong, it does not go any further and returns the return value of the last successfully executed animation.

First, here's how the Promise is written.

function chainAnimationsPromise(elem, animations) {
  // The variable ret is used to hold the return value of the last animation
  let ret = null;

  // Create a new, empty Promise
  let p = Promise.resolve();

  // Use the then method to add all animations
  for (let anim of animations) {
    p = p.then(function (val) {
      ret = val;
      return anim(elem);
    });
  }

  // return a Promise with an error-catching mechanism deployed
  return p
    .catch(function (e) {
      /* Ignore the error and continue execution */
    })
    .then(function () {
      return ret;
    });
}

Although the way Promise is written is a significant improvement over the way callback functions are written, at a glance the code looks like it's entirely Promise API (then, catch, etc.) and the semantics of the operation itself are not readily apparent.

Next is the way the Generator function is written.

function chainAnimationsGenerator(elem, animations) {
  return spawn(function* () {
    let ret = null;
    try {
      for (let anim of animations) {
        ret = yield anim(elem);
      }
    } catch (e) {
      /* Ignore the error and continue execution */
    }
    return ret;
  });
}

The above code uses the Generator function to iterate through each animation, and the semantics are much clearer than the Promise writeup, with all the user-defined actions appearing inside the spawn function. The problem with this way of writing is that there must be a task runner that automatically executes the Generator function, the spawn function in the above code is the auto-executor and it returns a Promise object and must ensure that the expression following the yield statement must return a Promise.

Finally, there is the way the async function is written.

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for (let anim of animations) {
      ret = await anim(elem);
    }
  } catch (e) {
    /* 忽略错误,继续执行 */
  }
  return ret;
}

As you can see the Async function is the cleanest and most semantically correct implementation, with little semantically irrelevant code. It takes the automatic executor in the Generator writing style and provides it at the language level instead, without exposing it to the user, so there is the least amount of code. With Generator writing, the auto-executor would need to be provided by the user.

Example: Completing asynchronous operations in sequence

In practice, it is common to encounter a set of asynchronous operations that need to be done in sequence. For example, reading a set of URLs in sequence and then outputting the results in the order they were read.

The Promise is written as follows.

function logInOrder(urls) {
  // Read all URLs remotely
  const textPromises = urls.map((url) => {
    return fetch(url).then((response) => response.text());
  });

  // Output in order
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text)));
  }, Promise.resolve()));
}

The above code uses the fetch method to read a set of URLs remotely at the same time. each fetch operation returns a Promise object that is placed in the textPromises array. The reduce method then processes each Promise object in turn, and then uses then to concatenate all the Promise objects, so that the results are output in turn.

This is a less intuitive and less readable way of writing. Here is the async function implementation.

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

The above code does simplify things considerably, the problem is that all remote operations are relayed. Only when the previous URL returns a result does it go on to read the next URL, which is inefficient and very time-consuming. What we need is to issue remote requests concurrently.

async function logInOrder(urls) {
  // Read the remote URL concurrently
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  // Output in order
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

In the above code, although the argument to the map method is the async function, it is executed concurrently because only the async function is executed internally in succession, not externally. The later for..of loop uses await internally, and therefore implements sequential output.

Top-level await

According to the syntax specification, the await command can only appear inside an async function, otherwise it will all report an error.

// error is reported
const data = await fetch("https://api.example.com");

In the above code, the await command is used independently and not placed inside the async function, and an error is reported.

There is now a syntax proposal that allows the await command to be used independently at the top level of the module, so that the above line of code will not report an error. The purpose of this proposal is to borrow await to solve the problem of asynchronous module loading.

// awaiting.js
let output;
async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
}
main();
export { output };

The output value of the module awaiting.js in the above code, output, depends on the asynchronous operation. We wrap the asynchronous operation inside an async function and call that function. Only when all the asynchronous operations inside are executed will the variable output have a value, otherwise it returns undefined.

The above code could also be written to execute the function immediately.

// awaiting.js
let output;
(async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };

Here is the writeup for loading this module.

// usage.js
import { output } from ". /awaiting.js";

function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

In the above code, the result of the execution of outputPlusValue() depends entirely on the time of execution. If the asynchronous operation in awaiting.js is not executed, the value of the loaded output will be undefined.

The current workaround is to have the original module output a Promise object from which to determine if the asynchronous operation has finished.

// awaiting.js
let output;
export default (async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };

In the above code, awaiting.js outputs a Promise object by default in addition to output (a Promise object is returned immediately after the async function is executed) from which to determine if the asynchronous operation has finished.

Here is a new way of writing this module when loaded.

// usage.js
import promise, { output } from ". /awaiting.js";

function outputPlusValue(value) { return output + value }

promise.then(() => {
  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100), 1000);
});

In the above code, the output of the awaiting.js object is placed inside promise.then(), which ensures that the output is not read until after the asynchronous operation is complete.

This is a bit more cumbersome and is tantamount to asking the user of the module to follow an additional usage protocol and use the module in a special way. If you forget to load it with Promise and just use the normal load method, the code that depends on the module could be wrong. Also, if the usage.js above has external output again, it is equivalent to all modules in this dependency chain being loaded using Promise.

The await command at the top is designed to solve this problem. It ensures that the module will only output values if the asynchronous operation completes.

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);

In the above code, both asynchronous operations are output with the await command added. Only when the asynchronous operation completes will this module output the value.

Loading this module is written as follows.

// usage.js
import { output } from ". /awaiting.js";
function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

The above code is written in exactly the same way as a normal module load. That is, the user of the module doesn't have to care at all if there are asynchronous operations inside the dependent module, just load it normally.

In this case, the module load will wait for the asynchronous operation of the dependent module (awaiting.js in the above example) to complete before executing the code that follows, kind of like pausing there. So, it will always get the correct output, and will not get a different value depending on the timing of the load.

Note that the top-level await can only be used in ES6 modules, not in CommonJS modules. This is because the CommonJS module's require() is loaded synchronously and cannot handle loading if it has a top-level await.

Here are some usage scenarios for top-level await.

// import() method loading
const strings = await import(`/i18n/${navigator.language}`);

// database operations
const connection = await dbConnector();

// Dependency rollback
let jQuery;
try {
  jQuery = await import("https://cdn-a.com/jQuery");
} catch {
  jQuery = await import("https://cdn-b.com/jQuery");
}

Note that if multiple modules containing the top-level await command are loaded, the load command is executed synchronously.

// x.js
console.log("x1");
await new Promise((r) => setTimeout(r, 1000));
console.log("X2");

// y.js
console.log("Y");

// z.js
import ". /x.js";
import ". /y.js";
console.log("Z");

The above code has three modules, the final z.js loads x.js and y.js and prints X1, Y, X2, Z. This means that z.js is not waiting for x.js to finish loading before loading y.js.

The top-level await command is a bit like that, surrendering execution of the code to other modules to load, and then taking execution back when the asynchronous operation is complete and continuing down the line.