ES6 async function


May 08, 2021 14:00 ES6


Table of contents


1. Meaning

ES2017 standard introduces async making asynchronous operation easier.

What is the async function? In a word, it's the Generator sugar of the Generator 语法糖

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

  1. const fs = require('fs');
  2. const readFile = function (fileName) {
  3. return new Promise(function (resolve, reject) {
  4. fs.readFile(fileName, function(error, data) {
  5. if (error) return reject(error);
  6. resolve(data);
  7. });
  8. });
  9. };
  10. const gen = function* () {
  11. const f1 = yield readFile('/etc/fstab');
  12. const f2 = yield readFile('/etc/shells');
  13. console.log(f1.toString());
  14. console.log(f2.toString());
  15. };

The function gen of the gen can be written as async function, and that's it.

  1. const asyncReadFile = async function () {
  2. const f1 = await readFile('/etc/fstab');
  3. const f2 = await readFile('/etc/shells');
  4. console.log(f1.toString());
  5. console.log(f2.toString());
  6. };

A comparison reveals that the async function replaces the generator function's asterisk with async and yield with await, that's all.

The improvement of the Generator function by the async function is reflected in the following four points.

(1) Built-in executor.

Generator functions must be executed by the executor, so there is a co module, and the async function brings its own executor. That is, the execution of the async function is exactly the same as the normal function, as long as one line.

  1. asyncReadFile();

The code above calls the asyncReadFile function, and then it executes automatically, outputing the final result. This is not like the Generator function at all, you need to call the next method, or use the co module, to actually execute and get the final result.

(2) Better semantics.

Async and await, the semantics are clearer than the asterisks and yields. Async indicates that there are asynchronous operations in the function, and await indicates that the expression that follows needs to wait for the result.

(3) Wider applicability.

The co module convention states that the yield command can only be followed by the Thunk function or the Promise object, while the await command of the async function can be followed by the Value of the Promise object and the value of the original type (values, strings, and Boolean values, but this is automatically converted to the Promise object of the immediate resolved).

(4) The return value is Promise.

The return value of the async function is the Promise object, which is much more convenient than the Generator function's return value is the Iterator object. You can specify the next step in the then method.

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

Basic usage

async function returns a Promise and you can use then method to add a callback function. When a function executes, it returns once it encounters await, waits until the asynchronous operation is complete, and then executes the statement that follows the function body.

Here's an example.

  1. async function getStockPriceByName(name) {
  2. const symbol = await getStockSymbol(name);
  3. const stockPrice = await getStockPrice(symbol);
  4. return stockPrice;
  5. }
  6. getStockPriceByName('goog').then(function (result) {
  7. console.log(result);
  8. });

The above code is a function for getting stock quotes, and the async keyword preceding the function indicates that there are asynchronous operations inside the function. When the function is called, a Promise object is returned immediately.

Here's another example, specifying how many milliseconds to output a value.

  1. function timeout(ms) {
  2. return new Promise((resolve) => {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. async function asyncPrint(value, ms) {
  7. await timeout(ms);
  8. console.log(value);
  9. }
  10. asyncPrint('hello world', 50);

The above code specifies that after 50 milliseconds, the hello world is output.

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

  1. async function timeout(ms) {
  2. await new Promise((resolve) => {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. async function asyncPrint(value, ms) {
  7. await timeout(ms);
  8. console.log(value);
  9. }
  10. asyncPrint('hello world', 50);

Async functions come in many forms of use.

  1. // 函数声明
  2. async function foo() {}
  3. // 函数表达式
  4. const foo = async function () {};
  5. // 对象的方法
  6. let obj = { async foo() {} };
  7. obj.foo().then(...)
  8. // Class 的方法
  9. class Storage {
  10. constructor() {
  11. this.cachePromise = caches.open('avatars');
  12. }
  13. async getAvatar(name) {
  14. const cache = await this.cachePromise;
  15. return cache.match(`/avatars/${name}.jpg`);
  16. }
  17. }
  18. const storage = new Storage();
  19. storage.getAvatar('jake').then(…);
  20. // 箭头函数
  21. const foo = async () => {};

3. Grammar

The syntax rules of the async function are generally simple, and the difficulty is the error handling mechanism.

Returns the Promise object

async function returns a Promise object.

The value returned by the return the async function becomes then to the then method callback function.

  1. async function f() {
  2. return 'hello world';
  3. }
  4. f().then(v => console.log(v))
  5. // "hello world"

In the above code, the value returned by the function f internal return command is received by the then method callback function.

async inside the async function causes the Promise object to become reject The wrong object thrown is received by the catch method callback function.

  1. async function f() {
  2. throw new Error('出错了');
  3. }
  4. f().then(
  5. v => console.log(v),
  6. e => console.log(e)
  7. )
  8. // Error: 出错了

The state of the Promise object changes

async by the async function must wait until the Promise object behind all internal await commands has been executed before a state change occurs unless a return statement is encountered or an error is thrown. That is, the callback function specified by the then method is executed only after the asynchronous operation inside the async function has been performed.

Here's an example.

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

In the code above, the getTitle has three actions inside: 抓取网页 the 取出文本 match the page 匹配页面标题 Only when all three operations are complete will the console in the then method be .log.

await command

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

  1. async function f() {
  2. // 等同于
  3. // return 123;
  4. return await 123;
  5. }
  6. f().then(v => console.log(v))
  7. // 123

In the code above, the parameter of the await command is the value 123, which is equivalent to return 123.

In another case, the await command is followed by a thenable object (that is, an object that defines the then method), and await equates it with a Promise object.

  1. class Sleep {
  2. constructor(timeout) {
  3. this.timeout = timeout;
  4. }
  5. then(resolve, reject) {
  6. const startTime = Date.now();
  7. setTimeout(
  8. () => resolve(Date.now() - startTime),
  9. this.timeout
  10. );
  11. }
  12. }
  13. (async () => {
  14. const sleepTime = await new Sleep(1000);
  15. console.log(sleepTime);
  16. })();
  17. // 1000

In the code above, 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 Promise processing.

This example also shows how to achieve hibernation. J avaScript has never had a sleeping syntax, but with the await command you can pause the program for a specified amount of time. A simplified sleep implementation is given below.

  1. function sleep(interval) {
  2. return new Promise(resolve => {
  3. setTimeout(resolve, interval);
  4. })
  5. }
  6. // 用法
  7. async function one2FiveInAsync() {
  8. for(let i = 1; i <= 5; i++) {
  9. console.log(i);
  10. await sleep(1000);
  11. }
  12. }
  13. one2FiveInAsync();

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

  1. async function f() {
  2. await Promise.reject('出错了');
  3. }
  4. f()
  5. .then(v => console.log(v))
  6. .catch(e => console.log(e))
  7. // 出错了

Note that in the code above, there is no return before the await statement, but the parameters of the reject method are still passed in to the callback function of the catch method. Here, if you add return before await, the effect is the same.

If the Promise object after any await statement becomes reject, the entire async function interrupts execution.

  1. async function f() {
  2. await Promise.reject('出错了');
  3. await Promise.resolve('hello world'); // 不会执行
  4. }

In the above code, the second await statement is not executed because the first await statement state becomes reject.

Sometimes, we want to not interrupt the asynchronous operation even if the previous asynchronous operation fails. T he first await can then be placed in try... inside the catch structure, so that the second await is executed regardless of whether the asynchronous operation succeeds or not.

  1. async function f() {
  2. try {
  3. await Promise.reject('出错了');
  4. } catch(e) {
  5. }
  6. return await Promise.resolve('hello world');
  7. }
  8. f()
  9. .then(v => console.log(v))
  10. // hello world

Another approach is for the Promise object after await to follow a catch method to handle previous errors.

  1. async function f() {
  2. await Promise.reject('出错了')
  3. .catch(e => console.log(e));
  4. return await Promise.resolve('hello world');
  5. }
  6. f()
  7. .then(v => console.log(v))
  8. // 出错了
  9. // hello world

Error handling

If an asynchronous operation after await goes wrong, the Promise object returned by the async function is rejected.

  1. async function f() {
  2. await new Promise(function (resolve, reject) {
  3. throw new Error('出错了');
  4. });
  5. }
  6. f()
  7. .then(v => console.log(v))
  8. .catch(e => console.log(e))
  9. // Error:出错了

In the above code, after the async function f is executed, the Promise object behind await throws an error object, causing the callback function of the catch method to be called, and its argument is the wrong object thrown. Specific execution mechanism, you can refer to the following "async function implementation principle."

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

  1. async function f() {
  2. try {
  3. await new Promise(function (resolve, reject) {
  4. throw new Error('出错了');
  5. });
  6. } catch(e) {
  7. }
  8. return await('hello world');
  9. }

If you have more than one await command, you can place it uniformly in try... catch structure.

  1. async function main() {
  2. try {
  3. const val1 = await firstStep();
  4. const val2 = await secondStep(val1);
  5. const val3 = await thirdStep(val1, val2);
  6. console.log('Final: ', val3);
  7. }
  8. catch (err) {
  9. console.error(err);
  10. }
  11. }

The following example uses try... catch structure, implementing multiple repeated attempts.

  1. const superagent = require('superagent');
  2. const NUM_RETRIES = 3;
  3. async function test() {
  4. let i;
  5. for (i = 0; i < NUM_RETRIES; ++i) {
  6. try {
  7. await superagent.get('http://google.com/this-throws-an-error');
  8. break;
  9. } catch(err) {}
  10. }
  11. console.log(i); // 3
  12. }
  13. test();

In the above code, if the await operation succeeds, the loop is exited with a break statement, and if it fails, it is captured by the catch statement and then goes to the next loop.

Use attention points

First, as already said earlier, the Promise object behind the await command may run as a rejected result, so it's best to put the await command in try... catch code block.

  1. async function myFunction() {
  2. try {
  3. await somethingThatReturnsAPromise();
  4. } catch (err) {
  5. console.log(err);
  6. }
  7. }
  8. // 另一种写法
  9. async function myFunction() {
  10. await somethingThatReturnsAPromise()
  11. .catch(function (err) {
  12. console.log(err);
  13. });
  14. }

Second, asynchronous operations following multiple await commands are best triggered at the same time if there is no secondary relationship.

  1. let foo = await getFoo();
  2. let bar = await getBar();

In the code above, getFoo and getBar are two separate asynchronous operations (i.e., not dependent on each other) that are written as secondary relationships. This is time-consuming because getBar is not executed until getFoo is complete, and they can be triggered at the same time.

  1. // 写法一
  2. let [foo, bar] = await Promise.all([getFoo(), getBar()]);
  3. // 写法二
  4. let fooPromise = getFoo();
  5. let barPromise = getBar();
  6. let foo = await fooPromise;
  7. let bar = await barPromise;

Both of these writings, getFoo and getBar, are triggered at the same time, which shortens the execution time of the program.

Third, the await command can only be used in async functions, and if used in normal functions, errors are reported.

  1. async function dbFuc(db) {
  2. let docs = [{}, {}, {}];
  3. // 报错
  4. docs.forEach(function (doc) {
  5. await db.post(doc);
  6. });
  7. }

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

  1. function dbFuc(db) { //这里不需要 async
  2. let docs = [{}, {}, {}];
  3. // 可能得到错误结果
  4. docs.forEach(async function (doc) {
  5. await db.post(doc);
  6. });
  7. }

The above code may not work because the three db.post operations will be performed in a synth, that is, at the same time, rather than as a secondary execution. The correct way to write is to use the for loop.

  1. async function dbFuc(db) {
  2. let docs = [{}, {}, {}];
  3. for (let doc of docs) {
  4. await db.post(doc);
  5. }
  6. }

Another approach is to use the array's reduce method.

  1. async function dbFuc(db) {
  2. let docs = [{}, {}, {}];
  3. await docs.reduce(async (_, doc) => {
  4. await _;
  5. await db.post(doc);
  6. }, undefined);
  7. }

In the example above, the first argument of the reduce method is the async function, so the first argument of the function is the Promise object returned by the previous operation, so you must wait for the operation to end with await. In addition, the reduce method returns the result of the execution of the async function of the last member of the docs array, and is also a Promise object, resulting in the addition of await before it.

If you do want multiple requests to execute in a synth, you can use the Promise.all method. When all three requests are resolved, the following two are written in the same way.

  1. async function dbFuc(db) {
  2. let docs = [{}, {}, {}];
  3. let promises = docs.map((doc) => db.post(doc));
  4. let results = await Promise.all(promises);
  5. console.log(results);
  6. }
  7. // 或者使用下面的写法
  8. async function dbFuc(db) {
  9. let docs = [{}, {}, {}];
  10. let promises = docs.map((doc) => db.post(doc));
  11. let results = [];
  12. for (let promise of promises) {
  13. results.push(await promise);
  14. }
  15. console.log(results);
  16. }

Fourth, the async function preserves the run stack.

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

In the above code, function a runs an asynchronous task b() inside. W hen b() runs, function a() does not break, but continues. B y the end of the b() run, it is possible that a() the run is over and the context in which b() is located has disappeared. If b() or c () is reported as incorrect, the error stack will not include a().

Now change this example to the async function.

  1. const a = async () => {
  2. await b();
  3. c();
  4. };

In the code above, when b() is running, a() is suspended and the context is saved. Once b() or c () is reported, the error stack will include a().

4. How the async function is implemented

The implementation principle of the async function is to Generator function and the 自动执行器 in a function.

  1. async function fn(args) {
  2. // ...
  3. }
  4. // 等同于
  5. function fn(args) {
  6. return spawn(function* () {
  7. // ...
  8. });
  9. }

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

Here is the implementation of the spawn function, which is basically a remake of the previous auto-executor.

  1. function spawn(genF) {
  2. return new Promise(function(resolve, reject) {
  3. const gen = genF();
  4. function step(nextF) {
  5. let next;
  6. try {
  7. next = nextF();
  8. } catch(e) {
  9. return reject(e);
  10. }
  11. if(next.done) {
  12. return resolve(next.value);
  13. }
  14. Promise.resolve(next.value).then(function(v) {
  15. step(function() { return gen.next(v); });
  16. }, function(e) {
  17. step(function() { return gen.throw(e); });
  18. });
  19. }
  20. step(function() { return gen.next(undefined); });
  21. });
  22. }

5. Comparison with other asynchronous processing methods

Let's look at an example of async function Promise to Generator and Generator functions.

Assuming that a DOM element is above, a series of animations are deployed, and the previous animation ends before the latest one can begin. If one of the animations goes wrong, it no longer executes down, returning the return value of the previous successfully executed animation.

The first is how Promise is written.

  1. function chainAnimationsPromise(elem, animations) {
  2. // 变量ret用来保存上一个动画的返回值
  3. let ret = null;
  4. // 新建一个空的Promise
  5. let p = Promise.resolve();
  6. // 使用then方法,添加所有动画
  7. for(let anim of animations) {
  8. p = p.then(function(val) {
  9. ret = val;
  10. return anim(elem);
  11. });
  12. }
  13. // 返回一个部署了错误捕捉机制的Promise
  14. return p.catch(function(e) {
  15. /* 忽略错误,继续执行 */
  16. }).then(function() {
  17. return ret;
  18. });
  19. }

Although Promise's writing is much better than that of callback functions, at first glance, the code looks entirely like Promise's API (then, catch, and so on), and the semantics of the operation itself are not easy to see.

This is followed by the writing of the Generator function.

  1. function chainAnimationsGenerator(elem, animations) {
  2. return spawn(function*() {
  3. let ret = null;
  4. try {
  5. for(let anim of animations) {
  6. ret = yield anim(elem);
  7. }
  8. } catch(e) {
  9. /* 忽略错误,继续执行 */
  10. }
  11. return ret;
  12. });
  13. }

The above code traverses each animation using the Generator function, the semantics are clearer than the Promise write, and all user-defined actions appear inside the spawn function. The problem with this writing is that there must be a task operator that automates the Generator function, and the spawn function in the code above is the auto-executor, which returns a Promise object, and must guarantee that the expression after the yield statement must return a Promise.

Finally, the async function is written.

  1. async function chainAnimationsAsync(elem, animations) {
  2. let ret = null;
  3. try {
  4. for(let anim of animations) {
  5. ret = await anim(elem);
  6. }
  7. } catch(e) {
  8. /* 忽略错误,继续执行 */
  9. }
  10. return ret;
  11. }

You can see that the Implementation of the Async function is the most concise, semantic, and has almost no semantic irrelevant code. I t changes the auto actuator in generator writing to be available at the language level and is not exposed to the user, so the amount of code is minimal. If you use generator writing, the auto-acter needs to be provided by the user.

6. Example: Asynchronous operations are completed in order

In real-world development, you often encounter a set of asynchronous operations that need to be done sequentially. For example, read a set of URLs remotely in turn, and then output the results in the order in which they were read.

Promise is written as follows.

  1. function logInOrder(urls) {
  2. // 远程读取所有URL
  3. const textPromises = urls.map(url => {
  4. return fetch(url).then(response => response.text());
  5. });
  6. // 按次序输出
  7. textPromises.reduce((chain, textPromise) => {
  8. return chain.then(() => textPromise)
  9. .then(text => console.log(text));
  10. }, Promise.resolve());
  11. }

The code above uses the fetch method while remotely reading a set of URLs. E ach fetch operation returns a Promise object and puts it into a textPromises array. The reduce method then processes each Promise object in turn, and then uses then to connect all Promise objects so that you can output the results in turn.

This kind of writing is not very intuitive and the readability is poor. Here's the async function implementation.

  1. async function logInOrder(urls) {
  2. for (const url of urls) {
  3. const response = await fetch(url);
  4. console.log(await response.text());
  5. }
  6. }

The code above is really simplified, and the problem is that all remote operations are secondary. O nly if the previous URL returns the result will the next URL be read, which is inefficient and a waste of time. What we need is to make remote requests in a synth.

  1. async function logInOrder(urls) {
  2. // 并发读取远程URL
  3. const textPromises = urls.map(async url => {
  4. const response = await fetch(url);
  5. return response.text();
  6. });
  7. // 按次序输出
  8. for (const textPromise of textPromises) {
  9. console.log(await textPromise);
  10. }
  11. }

In the above code, although the parameter of the map method is the async function, it is executed in a synth because only the inside of the async function is a secondary execution and the externality is not affected. B ack for: Await is used inside the of the loop, so sequential output is implemented.

7. Top-level await

Depending on the syntax specification, the await command can only appear inside the async function, otherwise errors will be reported.

  1. // 报错
  2. const data = await fetch('https://api.example.com');

In the above code, the await command is used independently and is not placed in the async function, which will report an error.

Currently, there is a syntax proposal that allows the await command to be used independently at the top of the module so that the above line of code is not misalmented. The purpose of this proposal is to borrow await to solve the problem of asynchronous loading of modules.

  1. // awaiting.js
  2. let output;
  3. async function main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. }
  8. main();
  9. export { output };

In the code above, the output value .js of the module awaiting is output, depending on the asynchronous operation. We wrap the asynchronous operation in an async function, and then call the function, and only if all the asynchronous operations inside are performed, the variable output will have a value, otherwise we will return undefined.

The above code can also be written in the form of an immediate execution function.

  1. // awaiting.js
  2. let output;
  3. (async function1 main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. })();
  8. export { output };

Here's how to load this module.

  1. // usage.js
  2. import { output } from "./awaiting.js";
  3. function outputPlusValue(value) { return output + value }
  4. console.log(outputPlusValue(100));
  5. setTimeout(() => console.log(outputPlusValue(100), 1000);

In the code above, the result of the execution of outputPlusValue() depends entirely on the time of execution. If the .js the asynchronous operation inside the content is not completed, the value of the output loaded in is undefined.

The current solution is to let the original module output a Promise object from which to determine if the asynchronous operation is over.

  1. // awaiting.js
  2. let output;
  3. export default (async function main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. })();
  8. export { output };

In the above code, the awaiting.js in addition to output output, also outputs a Promise object by default (after the async function executes immediately, returns a Promise object) from which to determine whether the asynchronous operation ends.

Here's a new way to load this module.

  1. // usage.js
  2. import promise, { output } from "./awaiting.js";
  3. function outputPlusValue(value) { return output + value }
  4. promise.then(() => {
  5. console.log(outputPlusValue(100));
  6. setTimeout(() => console.log(outputPlusValue(100), 1000);
  7. });

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

This writing is cumbersome and requires the user of the module to comply with an additional usage protocol and use the module in a special way. O nce you forget to load with Promise, using only the normal loading method, the code that depends on the module can go wrong. Also, if the .js above has external output, all modules of this dependency chain are loaded with Promise.

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

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

In the code above, both asynchronous operations are added to the await command at the output. The module will not output values until the asynchronous operation is complete.

Loading this module is written as follows.

  1. // usage.js
  2. import { output } from "./awaiting.js";
  3. function outputPlusValue(value) { return output + value }
  4. console.log(outputPlusValue(100));
  5. setTimeout(() => console.log(outputPlusValue(100), 1000);

The above code is written exactly the same as a normal module load. That is to say, the user of the module does not care at all, depending on the internal of the module there is no asynchronous operation, normal loading can be.

At this point, the module loads and waits for the asynchronous operation of the dependent module (in the case of awaiting.js above) to complete before executing the subsequent code, a bit like pausing there. Therefore, it always gets the correct output and does not get different values because of the different loading times.

Here are some usage scenarios for top-level await.

  1. // import() 方法加载
  2. const strings = await import( /i18n/${navigator.language} );
  3. // 数据库操作
  4. const connection = await dbConnector();
  5. // 依赖回滚
  6. let jQuery;
  7. try {
  8. jQuery = await import('https://cdn-a.com/jQuery');
  9. } catch {
  10. jQuery = await import('https://cdn-b.com/jQuery');
  11. }

Note that if you load more than one module that contains the top-level await command, the load command is executed synchronously.

  1. // x.js
  2. console.log("X1");
  3. await new Promise(r => setTimeout(r, 1000));
  4. console.log("X2");
  5. // y.js
  6. console.log("Y");
  7. // z.js
  8. import "./x.js";
  9. import "./y.js";
  10. console.log("Z");

The code above has three modules, the last z.js loads x.js and y .js, and the print results are X1, Y, X2, Z. This means that the z.js t wait for the x.js load to complete before loading the y.js.

The await command at the top level is a bit like handing over the execution power of the code to the other modules to load, and when the asynchronous operation is complete, take back the execution power and continue down.