async/await
本教程共 76 篇 · 第 13 篇 · 更新于 2026-07-25 · 约 4 分钟阅读
13. async/await
本节目标:async/await 语法、错误处理、并发控制与常见陷阱。
Promise 把嵌套拉平了,但满屏的 .then() 依然不够直观。ES2017 引入的 async/await,本质上就是 Promise 的语法糖,却能让异步代码读起来像同步代码一样自然。
我用它写过百万级数据迁移脚本,也用它维护过高并发网关。结论是:只要环境支持,async/await 应该是你处理异步的首选写法。
基本用法
在函数前面加 async,它就自动变成异步函数,返回值会被包成一个 Promise:
async function greet() {
return 'hello';
}
greet().then(v => console.log(v)); // hello
在 async 函数内部,你可以用 await 暂停执行,等右边的 Promise 落定后再继续:
import { readFile } from 'node:fs/promises';
async function readConfig() {
const data = await readFile('config.json', 'utf8');
const config = JSON.parse(data);
return config;
}
readConfig().then(cfg => console.log(cfg.port));
await 只在 async 函数内部有效。在非 async 函数里写 await 会报错。
Tip
await后面不一定非得是 Promise。如果是普通值,await会把它包成 resolved Promise,然后立刻返回。所以await 42结果是42。
错误处理:try/catch 回归
Promise 的错误处理靠 .catch(),而 async/await 让你重新用上熟悉的 try/catch:
import { readFile } from 'node:fs/promises';
async function loadUser() {
try {
const data = await readFile('user.json', 'utf8');
return JSON.parse(data);
} catch (err) {
console.error('读取失败:', err.message);
return null; // 或 throw err,取决于你的策略
}
}
这比在 .then() 链条里跳来跳去舒服多了。你也可以给 try/catch 配上 finally,做资源清理:
async function withCleanup() {
const conn = await createConnection();
try {
const result = await conn.query('SELECT * FROM users');
return result;
} finally {
await conn.close(); // 无论成功失败都会执行
}
}
并行执行:别一串 await 到底
async/await 有个经典陷阱:误以为它自动并行。看这段代码:
async function slow() {
const a = await readFile('a.txt', 'utf8');
const b = await readFile('b.txt', 'utf8');
const c = await readFile('c.txt', 'utf8');
return a + b + c;
}
三个文件互不相干,但这段代码是串行读的:读完 a 才读 b,读完 b 才读 c。总耗时是三者之和。
正确做法是先全部启动,再用 Promise.all 等待:
async function fast() {
const [a, b, c] = await Promise.all([
readFile('a.txt', 'utf8'),
readFile('b.txt', 'utf8'),
readFile('c.txt', 'utf8'),
]);
return a + b + c;
}
现在三个读取同时出发,总耗时取决于最慢的那个。
Warning在循环里逐个
await尤其容易中招。如果数组有 100 个元素,串行执行就是 100 倍耗时。需要并行时,要么Promise.all,要么控制并发数(后面会讲)。
顶层 await(Top-Level Await)
ES 模块(.mjs 文件或 "type": "module" 的 .js 文件)支持在模块顶层直接写 await,不用包 async 函数:
// config.mjs
import { readFile } from 'node:fs/promises';
const raw = await readFile('config.json', 'utf8');
export const config = JSON.parse(raw);
这在加载配置文件、初始化数据库连接时特别顺手。注意,顶层 await 会让模块变成异步加载,如果其他模块 import 它,也得等这个 await 完成。
CommonJS 不支持顶层 await,需要包在 async IIFE 里:
(async () => {
const data = await someAsyncOp();
module.exports = data;
})();
这也是 ESM 优先的又一个理由。
async 函数返回值
async 函数不管你怎么 return,结果都是 Promise:
async function demo() {
return 42;
}
// 等价于
function demo() {
return Promise.resolve(42);
}
如果 async 函数内部抛错,返回的就是 rejected Promise:
async function fail() {
throw new Error('oops');
}
fail().catch(e => console.error(e.message));
常见坑与对策
await 在 Array.prototype.forEach 里不等待
// 错误:forEach 不会等里面的异步操作
urls.forEach(async url => {
const res = await fetch(url);
console.log(res.status);
});
console.log('全部完成?不,这是先执行的');
用 for...of 串行执行,或者用 Promise.all + map 并行:
// 串行
for (const url of urls) {
const res = await fetch(url);
console.log(res.status);
}
// 并行
await Promise.all(urls.map(async url => {
const res = await fetch(url);
console.log(res.status);
}));
忘记 await 一个返回 Promise 的函数
async function save() {
writeFile('log.txt', 'data'); // 没 await!
console.log(' supposedly done');
}
这时 writeFile 的 Promise 没人管,出错你也抓不到。养成习惯:调用异步函数时,眼睛扫一眼有没有 await。