首页 / Node.js 教程 / async/await

Node.js 教程

async/await

本教程共 76 篇 · 第 13 篇 · 更新于 2026-07-25 · 约 4 分钟阅读

Node.jsasyncawait异步并发

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