回调地狱与演进
早期 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);
// ... 越来越深
});
});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。
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),否则会串行变慢。
错误传播
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; // 想让上层也感知就再抛出
}
}孤立的 promise.catch 缺失会在 Node 中触发 unhandledRejection。生产进程应监听该事件并优雅退出。
并行组合
并行执行多个独立异步任务用 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));竞速与超时
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);取消 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('已取消');
}常见陷阱与 FAQ
每次 await 都等上一次完成,变成串行。若任务相互独立,先收集 Promise.all(tasks) 再等待。
整体立即 reject,其余任务仍在后台跑完但不等结果。需要"全部跑完看结果"用 allSettled。
函数返回 Promise 而非结果,后续若拿它当值用会得到 [object Promise] 或 undefined。常见 bug:const x = doAsync() 少了 await。