第02章:AI帮我用了async/await但没有处理未捕获的Promise rejection

第02章:AI帮我用了async/await但没有处理未捕获的Promise rejection

“Node.js v24 的 unhandledRejection 默认会让进程崩溃(–unhandled-rejections=throw,从 v15 开始是默认行为)。AI 帮你把所有回调改成 async/await,代码更整洁了,但哪个 await 没有 try-catch 就是一个潜在的进程崩溃点。你在开发环境没遇到这个路径,测试也没覆盖,到了生产才发现某个小功能会让整个服务崩溃。”


ℹ️ 版本说明:本章基于 Node.js v24.16.0

2.1 AI默认会生成什么

// AI 通常给你的代码(async/await,但没有错误处理)
async function fetchUserData(userId) {
  const user = await db.findById(userId);           // 如果抛出呢?
  const orders = await db.getOrders(userId);         // 如果抛出呢?
  const profile = await externalApi.getProfile(userId); // 网络超时呢?
  
  return { user, orders, profile };
}

// 调用时没有 catch
app.get('/user/:id', async (req, res) => {
  const data = await fetchUserData(req.params.id);  // 未捕获异常!
  res.json(data);
});

2.2 AI通常遗漏的4个坑

⚠️ 坑1:Promise.all 中任意一个失败会让所有结果都丢失

// 危险:一个失败,全部失败
const [users, orders, products] = await Promise.all([
  db.getUsers(),      // 如果这个成功了
  db.getOrders(),     // 这个失败了
  db.getProducts()    // 这个也成功了
]);
// 结果:抛出异常,users 和 products 的结果全丢失

// 更安全:Promise.allSettled(不管成功失败都等待)
const results = await Promise.allSettled([
  db.getUsers(),
  db.getOrders(),
  db.getProducts()
]);

const [usersResult, ordersResult, productsResult] = results;
if (usersResult.status === 'fulfilled') {
  const users = usersResult.value;
}
if (ordersResult.status === 'rejected') {
  logger.warn({ err: ordersResult.reason }, 'Failed to fetch orders');
  // 优雅降级:orders 用空数组代替
}

⚠️ 坑2:forEach 里用 async/await 不能正确捕获错误

// 危险:forEach 不等待 async 函数,错误无法被外层 catch 捕获
async function processUsers(users) {
  users.forEach(async (user) => {
    await sendEmail(user.email);  // 失败了也不影响 forEach 继续
  });
  // forEach 结束时,sendEmail 可能还没完成
}

// 正确方案1:for...of(顺序执行)
async function processUsers(users) {
  for (const user of users) {
    try {
      await sendEmail(user.email);
    } catch (err) {
      logger.error({ err, userId: user.id }, 'Failed to send email');
      // 一个失败,继续处理下一个
    }
  }
}

// 正确方案2:Promise.allSettled(并行执行,单个失败不影响其他)
async function processUsers(users) {
  const results = await Promise.allSettled(
    users.map(user => sendEmail(user.email))
  );
  const failures = results.filter(r => r.status === 'rejected');
  if (failures.length > 0) {
    logger.error({ count: failures.length }, 'Some emails failed');
  }
}

⚠️ 坑3:超时未设置导致 Promise 永远挂起

// 危险:没有超时,外部 API 不响应时 Promise 永远挂起
const data = await fetch('https://api.example.com/data');

// 正确:设置超时(Node.js v24 fetch 内置 AbortController)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); // 5秒超时

try {
  const response = await fetch('https://api.example.com/data', {
    signal: controller.signal
  });
  const data = await response.json();
} catch (err) {
  if (err.name === 'AbortError') {
    throw new Error('Request timed out after 5 seconds');
  }
  throw err;
} finally {
  clearTimeout(timeout);
}

⚠️ 坑4:错误被"吃掉"(silent catch)

// 危险:空 catch 块,错误被静默忽略
async function updateCache(key, value) {
  try {
    await redis.set(key, value);
  } catch (err) {
    // 什么都不做!
    // 错误消失了,调用方不知道 cache 更新失败了
  }
}

// 正确:至少记录日志,根据业务决定是否重新抛出
async function updateCache(key, value) {
  try {
    await redis.set(key, value);
  } catch (err) {
    logger.warn({ err, key }, 'Cache update failed, continuing without cache');
    // 如果是可选功能(缓存),记录警告后继续
    // 如果是关键功能,throw err 让调用方处理
  }
}

2.3 更好的提示词

提示词 P01:审查并修复 async/await 错误处理

使用时机:让 AI 帮你找出代码里潜在的 unhandled rejection

帮我审查以下 Node.js v24.16.0 代码,找出所有可能导致 unhandledPromiseRejection 的地方,并给出修复方案。

代码:
[粘贴你的代码]

检查重点:
1. 哪些 await 没有被 try-catch 包裹?
2. 哪些 async 函数的调用没有 .catch() 或 try-catch?
3. forEach 里是否用了 async/await?(不能正确捕获错误)
4. Promise.all 是否需要改为 Promise.allSettled?
5. 有无设置超时的外部 API 调用?
6. 有无空 catch 块(silently swallowing errors)?

对于每个问题:
- 说明风险(可能导致什么后果)
- 给出修复后的代码
- 解释为什么这样修复

额外要求:
- 生产环境中,哪些错误应该让进程崩溃(严重错误)?
- 哪些错误应该记录日志后继续运行(可恢复错误)?

基于 Node.js v24.16.0。

提示词 P02:并发控制和批处理

使用时机:需要同时处理多个 Promise,避免并发太高或 Promise.all 全部失败

帮我为 Node.js v24.16.0 应用设计一个健壮的并发处理方案。

场景:我需要并发处理一批用户(可能有1000个),对每个用户调用外部 API 发送邮件。

问题:
1. 同时发1000个请求会让外部 API 过载
2. Promise.all 里一个失败会让全部结果丢失
3. 部分失败时需要记录哪些成功哪些失败

方案:

1. 使用 p-limit 限制并发数(同时最多10个请求):
   import pLimit from 'p-limit';
   const limit = pLimit(10);
   const results = await Promise.allSettled(
     users.map(user => limit(() => sendEmail(user)))
   );

2. 使用 Promise.allSettled 处理部分失败:
   - 成功的记录结果
   - 失败的记录错误和用户 ID(方便重试)

3. 分批处理(每批100个,防止内存过高):
   如何把1000个用户分成每批100个,顺序处理每批?

4. 重试机制(网络错误时自动重试):
   - 使用 p-retry 或手动实现指数退避重试
   - 哪些错误应该重试?哪些不应该(如4xx)?

5. 进度报告(处理1000个用户时,每100个报告一次进度)

给我完整的实现代码。

基于 Node.js v24.16.0。

提示词 P03:Node.js v24 的原生 async 工具

使用时机:了解 Node.js 内置的 async 工具,减少第三方依赖

帮我了解 Node.js v24.16.0 内置的原生 async 工具,减少第三方依赖。

涵盖以下内容:

1. Promise 方法完整对比:
   - Promise.all:所有成功或任意失败
   - Promise.allSettled:等待所有,不管成功失败
   - Promise.any:任意一个成功就够了(竞速取最快成功的)
   - Promise.race:第一个完成(不管成功失败)
   
   每个方法给一个实际使用场景。

2. AbortController(内置超时和取消):
   - HTTP 请求超时
   - 取消正在进行的数据库查询
   - 配合 EventEmitter 取消长时间任务

3. AsyncLocalStorage(不用传参就能传 requestId):
   - 在中间件里设置 requestId
   - 在任意深度的函数里获取当前 requestId
   - 比 req.locals 更优雅

4. stream/promises(File/Stream 操作的 async 版本):
   - fs.promises(文件操作)
   - stream.pipeline(替代 pipe,可以 await)

基于 Node.js v24.16.0,尽量使用内置模块。

2.4 验收清单

检查项 验证方法 AI辅助
所有 await 都有错误处理 ESLint @typescript-eslint/no-floating-promises 用 P01 审查代码
forEach 不用 async/await 搜索 forEach.*async 用 P01 发现并修复
外部 API 调用有超时 代码里有 AbortController + setTimeout 用 P01 和 P03 修复
Promise.all 考虑用 allSettled 批量操作用 Promise.allSettled 用 P02 设计方案
无空 catch 块 ESLint no-empty 规则 用 P01 检查
并发数有限制(批量操作) 使用 p-limit 或分批处理 用 P02 实现

2.5 本章小结

如果你只记一件事Promise.allSettled 是比 Promise.all 更安全的默认选择——当你需要并发执行多个操作且可以接受部分失败时,用 allSettled 而不是 allall 在任意一个 Promise reject 时就全部失败,allSettled 会等待所有 Promise,让你决定如何处理每个结果。

Async/await 错误处理的三个层次

  1. 单个 await 的 try-catch(函数层):区分可恢复错误(记录日志继续)和不可恢复错误(抛出给调用方)
  2. 批量 Promise 的 allSettled(并发层):部分失败时不影响其他,收集所有失败统一处理
  3. process 级别的 unhandledRejection(进程层):最后一道防线,确保未捕获的 rejection 被记录,而不是静默丢失

→ 第3章:AI帮我配置了package.json但没有锁定依赖版本