Node.js 开发手册NODE.JS

CHAPTER 03异步核心 · ASYNC

异步编程范式

从回调到 async/await:错误传播、Promise 组合与取消。写出既清晰又正确的异步代码。

开始阅读难度进阶先修第二、三章约2小时
03.1

回调地狱与演进

早期 Node 大量使用错误优先回调(err-first callback)。嵌套过深形成"回调地狱",可读性与错误处理都很差。Promise 与 async/await 逐步将其扁平化。

// 回调地狱(反例)
fs.readFile('a', (err, a) => {
  if (err) return cb(err);
  fs.readFile(a, (err, b) => {
    if (err) return cb(err);
    // ... 越来越深
  });
});
03.2

Promise 基础

Promise 表示一个未来的值,有三种状态:pending / fulfilled / rejected。用 then/catch/finally 链式处理。

const p = new Promise((resolve, reject) => {
  someAsync((err, val) => err ? reject(err) : resolve(val));
});
p.then(v => console.log(v))
 .catch(e => console.error(e))
 .finally(() => console.log('done'));
永远返回/抛出

在 then 里忘记 return 会让后续拿到 undefined。链式里保持每段都返回 Promise。

03.3

async/await

async 函数总是返回 Promise,await 暂停执行直到 Promise 完成,让异步代码读起来像同步。

async function load() {
  const a = await read('a');      // 暂停等待
  const b = await read(a);        // 串行
  return b;
}
// 顶层 await 在 ESM 模块中可直接使用
const cfg = await import('./config.mjs');
顺序很关键

需要并行时别在循环里逐个 await。先收集 Promise 数组再用 Promise.all(见 03.5),否则会串行变慢。

03.4

错误传播

async 函数内抛出的异常会变成 rejected Promise,沿调用链向上。用 try/catch 包围 await,或在调用处 catch。

async function main() {
  try {
    const data = await risky();
    return process(data);
  } catch (e) {
    console.error('失败:', e.message);
    throw e;            // 想让上层也感知就再抛出
  }
}
未捕获的 rejection

孤立的 promise.catch 缺失会在 Node 中触发 unhandledRejection。生产进程应监听该事件并优雅退出。

03.5

并行组合

并行执行多个独立异步任务用 Promise.all(任一失败即整体失败)、allSettled(等待全部、记录成败)、any(取第一个成功)。

const [u, o] = await Promise.all([getUser(), getOrders()]);

// 容忍失败
const results = await Promise.allSettled([task1(), task2()]);
results.forEach(r => r.status === 'fulfilled'
  ? console.log(r.value) : console.warn(r.reason));
03.6

竞速与超时

Promise.race 取第一个敲定者,常用于给慢请求加超时。注意:未赢的 Promise 仍会执行,需配合取消。

function withTimeout(p, ms) {
  const t = new Promise((_, rej) =>
    setTimeout(() => rej(new Error('timeout')), ms));
  return Promise.race([p, t]);
}
const data = await withTimeout(fetch(url), 3000);
03.7

取消 AbortController

Node 原生 fetch、流与定时器都支持 AbortSignal。用 AbortController 主动取消进行中的操作,避免资源浪费。

const ac = new AbortController();
setTimeout(() => ac.abort(), 2000);   // 2 秒后取消
try {
  await fetch(url, { signal: ac.signal });
} catch (e) {
  if (e.name === 'AbortError') console.log('已取消');
}
03.8

常见陷阱与 FAQ

for 循环里 await 为什么慢?

每次 await 都等上一次完成,变成串行。若任务相互独立,先收集 Promise.all(tasks) 再等待。

Promise.all 一个失败会怎样?

整体立即 reject,其余任务仍在后台跑完但不等结果。需要"全部跑完看结果"用 allSettled。

异步函数里忘了 await 会怎样?

函数返回 Promise 而非结果,后续若拿它当值用会得到 [object Promise] 或 undefined。常见 bug:const x = doAsync() 少了 await。

03.9

实战练习与资源

延伸资源