JavaScript · Promise 与 async/await

一个单向状态机 + 一条错误滑道 — then 返回的还是 Promise, async 函数只是把 then 链写成了顺序的样子

resolve(v) reject(e) Promise 状态机 (单向不可逆) pending 等待中, 可博一次 fulfilled 带值 v, 走 then rejected 带原因 e, 走 catch 落定后冻结: 再 resolve/reject 一律无效 then 链: 每节返回新 Promise then A then B catch C then D 错误滑道: B 抛错后跳过 C 之前所有成功回调, 在 catch 接住并"复位" catch 返回值让链恢复 fulfilled → D 正常执行 链中每节: 返回值传给下一节 · return promise 会被展开等待 finally: 不改值不接错, 只做"必跑"的收尾 组合器四兄弟 all 全成才成, 一败俱败 (快失败) allSettled 全收成/败明细, 容忍部分失败 race 第一个落定的说了算(含失败) any 第一个成功的赢, 全败才 AggregateError // async/await 的真面目: 同步写法 = then 链, await 之后全是微任务 async function load() { const a = await fetchA(); const b = await fetchB(a); return b.data; } // 等价的 Promise 链写法 (每行一个 await = 一节 then) fetchA().then(a => fetchB(a)) .then(b => b.data); // try/catch around await = .catch 挂在链尾 try { await load(); } catch (e) { log(e); } // 心智模型: await 处函数"让出栈", 剩余部分进微任务排队 —— 见事件循环页 // async 函数永远返回 Promise: return 5 实际是 Promise.resolve(5), throw 实际是 rejected Legend pending fulfilled / 成功回调 rejected / 错误滑道 组合器 状态单向 · 每节新 Promise · 错误沿滑道找 catch · async 是 then 链的语法糖

状态单向不可逆

  • • pending → fulfilled / rejected 各一次
  • • 落定后 resolve/reject 全部无效
  • • then 注册的回调照样收到落定值
  • • executor 同步执行, 别在里面干重活

错误沿滑道走

  • • 链上任一节抛错/ reject 滑到 catch
  • • catch 返回值让链"复位"继续 fulfilled
  • • 没人接的 rejection = 未处理异常
  • • await 的 try/catch 就是链尾 .catch

组合器选型

  • • 依赖齐全才继续: all(快失败)
  • • 报表容忍部分失败: allSettled
  • • 超时/竞速: race(记得取消慢的)
  • • 多源容灾取首个成功: any

💡 一句话理解

Promise 是一张只能兑现一次的票据: 要么resolve(带值), 要么reject(带原因), 落定后永久冻结。then 链是一串"拿到上一步结果做下一步"的接力 — 每一节返回的还是一个新 Promise, 所以能无限链下去; 任何一节出问题, 错误会像坐滑道一样越过所有成功回调, 直到被某个 catch 接住, 而接住后链又恢复正常。

async/await 没有引入任何新机制: async 函数永远返回 Promise, await 后面的代码等价于挂进 then 的微任务。它赢在把控制流写回了从上到下的顺序 — try/catch、for 循环、变量作用域全部回归同步直觉。理解了"票据 + 滑道", 组合器 all/race/allSettled/any 只是四种收票策略。

🧠 必知必会 必考 & 必会

三状态与冻结
pending → fulfilled/rejected 单向; 落定后再调用 resolve/reject 无效, 但之后的代码照跑(要自己防副作用)。
new Promise((res) => { res(1); res(2); console.log('after'); })
  .then(v => log(v));
// 输出: after → 1   (第二次 res 无效, after 先同步跑)
executor 同步执行
传给 Promise 的函数立即同步执行; 它内部 throw 等价 reject, 但 setTimeout 里 throw 不会。
console.log('before');
new Promise(() => console.log('executor'));
console.log('after');
// → before executor after  (executor 是同步的)
then 返回新 Promise
每节 then 产生全新 Promise, 链的下一步等的是上一节的返回值, 不是最初的那个。
Promise.resolve(1)
  .then(v => v + 1)
  .then(v => log(v));   // → 2  接力传值
return promise 会被展开
then 里返回一个 Promise, 链会等它落定再继续 — 这是串行异步的基石。
p.then(() => delay(100))   // 返回 promise
  .then(() => log('100ms 后')); // 等它落定才跑
错误滑道
链中 reject/throw 越过后续成功回调直达最近 catch; catch 的返回值让链恢复 fulfilled。
Promise.reject('e')
  .then(() => log('跳过我'))
  .catch(e => '恢复:' + e)
  .then(v => log(v));   // → 恢复:e
catch 位置语义
catch 挂在哪, 就只管它前面的链; 之后的错误要再往后的 catch 才接得住。
fetchA()
  .catch(recoverA)     // 只管 fetchA
  .then(useData)
  .catch(finalLog);    // 管 recoverA 之后的全部
finally
不接参数、不改链上的值; 抛错才会改变链的状态。适合关 loading/释放资源。
loading = true;
await fetchX().finally(() => loading = false);
// 关键: 成功失败都关 loading, 且不影响返回值
all / allSettled
all 全成才成(一败即败, 其余结果丢弃); allSettled 永远 fulfilled, 给出每个的 status 明细。
const rs = await Promise.allSettled([a, b, c]);
rs.filter(r => r.status === 'rejected')   // 收集失败
  .forEach(r => log(r.reason));
race / any
race 取第一个落定(成功失败都算); any 取第一个成功, 全败抛 AggregateError。
const fastest = await Promise.race([cdn1(), cdn2()]);
const ok = await Promise.any([backup1(), backup2()]);
async 返回包装
async 函数 return 5 ≡ Promise.resolve(5); throw ≡ rejected; 返回值永远能 .then。
async function f() { return 5; }
f() instanceof Promise;   // → true
f().then(log);            // → 5
await 直通
await 非 thenable 值直接当同步用(await 42 → 42), 但仍多一个微任务 tick; 热循环里别滥 await。
const x = await 42;   // → 42 合法但没意义
// await p 的 p 若已落定, 只剩一次微任务开销
取消不在 Promise 里
Promise 本身不可取消; 现代做法把取消信号外置成 AbortSignal, 传进 fetch/事件。
const ac = new AbortController();
fetch(url, { signal: ac.signal });
ac.abort();   // 关键: 请求真正取消, abort 事件触发

🏭 生产实战 real world

场景 1 · 接口超时: race 竞速 + AbortController 真取消

第三方接口偶发 30s 不响应拖死页面。超时只是"不等了", 还要把慢请求真正掐断释放连接:

async function withTimeout(ms) {
  const ac = new AbortController();
  const t = setTimeout(() => ac.abort(), ms);
  try {
    return await fetch(url, { signal: ac.signal });
  } catch (e) {
    if (e.name === 'AbortError') throw new Error('TIMEOUT');
    throw e;
  } finally { clearTimeout(t); }
}

用 race 只解决"不等", 请求还在跑占连接; abort 才是真取消, P99 尾部从 30s 消失。

场景 2 · 并发限制: 1000 个任务只放 8 个在飞

全量 Promise.all 会瞬间打爆下游, 手写 mapLimit 用"工位"控制并发:

async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let i = 0;
  async function worker() {
    while (i < items.length) {          // 关键: 抢下一个工位
      const idx = i++;
      results[idx] = await fn(items[idx]);
    }
  }
  await Promise.all(Array.from({ length: limit }, worker));
  return results;
}
await mapLimit(urls, 8, download);

场景 3 · 重试 + 指数退避: 只重试"值得重试"的错误

网络抖动该重试, 4xx 业务错重试也是白打。区分错误类型 + 退避 + 上限:

async function retry(fn, { times = 3, base = 200 } = {}) {
  for (let i = 0; ; i++) {
    try { return await fn(); }
    catch (e) {
      if (i >= times - 1 || !e.retryable) throw e;  // 关键: 白名单
      await new Promise(r => setTimeout(r, base * 2 ** i + Math.random() * 100));
    }
  }
}
// 5xx/网络错标 retryable: true; 4xx 校验错不重试

场景 4 · 报表聚合: allSettled 容忍部分数据源挂掉

大盘页同时拉 5 个数据源, 一个挂了不该整页 500。收集成败明细, 失败的模块降级展示:

const results = await Promise.allSettled([sales(), inventory(), ads()]);
const data = {}, failed = [];
for (const [i, r] of results.entries()) {
  if (r.status === 'fulfilled') data[sources[i].name] = r.value;
  else failed.push({ name: sources[i].name, err: String(r.reason) });
}
logger.warn('partial failure', { failed });   // 可观测而非静默

场景 5 · 回调金字塔迁移: 三层嵌套变顺序代码

老代码 fs.readFile 套三层, 错误要在每层处理。迁移后 try/catch 一处收口:

// 迁移前: read(a, (e1, a) => read(b(a), (e2, b) => ...)) 三层嵌套
// 迁移后:
async function pipeline() {
  try {
    const a = await readFileA();
    const b = await readFileB(a);      // 关键: 依赖前一步的顺序逻辑
    return merge(a, b);
  } catch (e) {                        // 三步的错误一处收口
    log.error('pipeline', e); throw e;
  }
}

场景 6 · single-flight: 缓存击穿时 100 个请求只打 1 个

热点 key 失效瞬间, 并发 100 个请求同时回源。让它们共享同一个 in-flight Promise:

const inflight = new Map();
async function getSingleFlight(key) {
  if (cache.has(key)) return cache.get(key);
  if (inflight.has(key)) return inflight.get(key);   // 关键: 搭车
  const p = loadFromDb(key)
    .then(v => { cache.set(key, v); return v; })
    .finally(() => inflight.delete(key));
  inflight.set(key, p);
  return p;
}

击穿瞬间 DB 回源 QPS 从 100 → 1, Promise 作为"可共享的未来值"的精髓用法。

场景 7 · 写操作串行队列: 防止并发写把状态写花

同一账户的扣款请求并发到达, 读-改-写交错导致余额错乱。用 Promise 链把写操作收敛成串行:

class WriteQueue {
  #tail = Promise.resolve();          // 串行化的链尾
  enqueue(task) {
    const p = this.#tail.then(task);  // 关键: 排在上一单之后
    this.#tail = p.catch(() => {});   // 出错不断链
    return p;
  }
}
const q = new WriteQueue();
q.enqueue(() => debit(accountId, 100));

场景 8 · 统一错误翻译层: 第三方错误不穿透业务

SDK 抛的错五花八门, 上层没法分支。封装层把一切翻译成业务错误类型:

async function callPay(order) {
  try { return { ok: true, data: await sdk.pay(order) }; }
  catch (e) {
    if (e.code === 'INSUFFICIENT_FUNDS') return { ok: false, kind: 'NO_MONEY' };
    if (e.name === 'TimeoutError')      return { ok: false, kind: 'TIMEOUT', retryable: true };
    logger.error('pay unknown', e);
    return { ok: false, kind: 'UNKNOWN' };   // 上层永远拿到可分支的形状
  }
}

场景 9 · 懒初始化单例: 把"连接"缓存成 Promise

DB 连接初始化有并发窗口: 缓存Promise 本身而不是结果, 天然防并发重复初始化:

let connP = null;
function getConn() {
  if (!connP) {
    connP = createConn().catch(e => { connP = null; throw e; });
    // 关键: 失败要清掉缓存的 rejected promise, 否则永远失败
  }
  return connP;
}
const c = await getConn();

场景 10 · 事件转 Promise: 一次性等待 + 超时兜底

等 WebSocket "ready" 消息再继续, 同时防对方永远不来。once + race 组合:

function waitFor(emitter, event, ms) {
  return Promise.race([
    new Promise(res => emitter.once(event, res)),
    new Promise((_, rej) =>
      setTimeout(() => rej(new Error('event timeout')), ms)),
  ]);
}
await waitFor(ws, 'ready', 5000);   // 拿到 ready 或 5s 报超时

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 构造器既不 resolve 也不 reject — 永远 pending, 下游全部挂起且无报错. 正解: 每条路径必须落定, 外层套超时兜底。
// 错: new Promise((res, rej) => { if (x) res(1); });  // x 假时悬挂
// 对: if (x) res(1); else rej('no x');
坑 2 · then 里忘 return — 链断裂, 下节拿到 undefined, 异步结果没人等. 正解: 链中必 return。
// 错: p.then(d => { save(d); }).then(r => log(r)); // r 是 undefined
// 对: p.then(d => save(d)).then(r => log(r));
坑 3 · 以为 await 在循环里是并发 — for 里 await 是串行, 尾延迟=总和. 正解: 并发用 Promise.all, 要限流用 mapLimit。
// 错: for (id of ids) await fetch(id);  // 100×200ms=20s
// 对: await Promise.all(ids.map(id => fetch(id)));  // ~200ms
坑 4 · all 一败俱败 — 10 个请求 1 个 404, 其余 9 个成功结果全丢. 正解: 容忍部分失败用 allSettled。
// 错: await Promise.all([a, b, c]);   // b 挂全挂
// 对: await Promise.allSettled([a, b, c]);
坑 5 · race 后不取消慢请求 — 超时赢了, 底层连接还在跑, 占连接池. 正解: AbortController 真取消。
// 错: Promise.race([fetch(u), timeout(1000)]); // fetch 还在跑
// 对: fetch(u, { signal: ac.signal }) + setTimeout(() => ac.abort(), 1000)
坑 6 · async 函数返回值当同步用 — 拿到的是 Promise. 正解: 调用侧 await 或 .then。
// 错: const u = getUser(); u.name;      // undefined
// 对: const u = await getUser(); u.name;
坑 7 · executor 里 throw 后异步 throw — executor 同步 throw 会 reject, 但异步回调里 throw 砸到全局. 正解: 异步段显式 reject。
// 错: new Promise((res, rej) => setTimeout(() => { throw 1; }));
// 对: new Promise((res, rej) => setTimeout(() => rej(1)));
坑 8 · new Promise 包已有 Promise — 无意义的"promise 套壳"反模式. 正解: 直接链式/await。
// 错: new Promise(res => fetchX().then(res));
// 对: fetchX();   // 它本来就是 Promise
坑 9 · resolve 后继续跑副作用 — 状态冻结但函数没停, 后续代码照常执行. 正解: resolve 后立刻 return。
// 错: res(v); startHeavyWork();   // 还在干活
// 对: return res(v);
坑 10 · catch 之后以为错误"处理完不会传播" — catch 里再 throw 会继续向下游滑. 正解: 想终止就正常返回, 想透传就 throw。
p.catch(e => { log(e); throw e; })   // 记录后继续向上滑
  .catch(final);                     // 这里还能接到
坑 11 · finally 里 return 吞值 — finally 返回值会覆盖链上的值(和 try/finally 同款). 正解: finally 里别 return。
// 错: p.finally(() => 'oops')  // 链值被覆盖成 'oops'
// 对: p.finally(() => cleanup())  // 返回 promise 会被等待
坑 12 · then(f, g) 两参写法的坑 — g 接不到 f 自己抛的错(同一节的回调二选一). 正解: 错误处理用独立 .catch。
// 错: p.then(v => risky(v), e => fix(e)); // risky 抛错没人接
// 对: p.then(v => risky(v)).catch(e => fix(e));
坑 13 · 空 all/race 永远悬挂 — all([]) 是 resolve([]) 但 race([]) 永远 pending. 正解: 先判空。
Promise.race([]);   // → 永远 pending!
// 对: if (!list.length) return default; else Promise.race(list)
坑 14 · 缓存了 rejected promise — 单例初始化失败后缓存里是 rejected, 以后全失败. 正解: 失败时清缓存。
connP = create().catch(e => { connP = null; throw e; });
// 关键: 自愈, 下次重新初始化
坑 15 · unhandledrejection 静默 — 没 catch 的 rejection 在 Node 15+ 直接崩进程, 浏览器只告警. 正解: 全局兜底上报 + 源头补 catch。
process.on('unhandledRejection', (r) => report(r));
// 但兜底不是 excuse: 每个 await 链要有明确错误去向
坑 16 · forEach + async — forEach 不等待, "顺序处理"变全并发. 正解: for...of 串行或 mapLimit 并发。
// 错: list.forEach(async x => await send(x)); // 立刻全部发出
// 对: for (const x of list) await send(x);
坑 17 · race 空数组悬挂 — race([]) 永远 pending. 正解: 判空兜底。
// 对: const winner = ps.length ? Promise.race(ps) : Promise.resolve(null);
坑 18 · async 回调在非 async API 里失效 — addEventListener 的 async 回调错误没人接, 变 unhandled. 正解: 回调内部自己 try/catch。
btn.onclick = async () => {
  try { await save(); } catch (e) { show(e); }  // 自包错误
};
坑 19 · 微任务里做重活 — then 链里跑大计算 = 阻塞后续微任务与渲染. 正解: 重活切片或进 Worker。
// 错: p.then(d => JSON.parse(huge))  // 阻塞 300ms
// 对: worker.parse(huge)  或切片处理
坑 20 · await 丢并行机会 — 两个独立请求先 await a 再 await b, 白串行. 正解: 先同时发起, 再分别 await。
// 错: const a = await f1(); const b = await f2(); // 串行
// 对: const pa = f1(), pb = f2(); const a = await pa, b = await pb;