第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 而不是 all。all 在任意一个 Promise reject 时就全部失败,allSettled 会等待所有 Promise,让你决定如何处理每个结果。
Async/await 错误处理的三个层次:
- 单个 await 的 try-catch(函数层):区分可恢复错误(记录日志继续)和不可恢复错误(抛出给调用方)
- 批量 Promise 的 allSettled(并发层):部分失败时不影响其他,收集所有失败统一处理
- process 级别的 unhandledRejection(进程层):最后一道防线,确保未捕获的 rejection 被记录,而不是静默丢失
→ 第3章:AI帮我配置了package.json但没有锁定依赖版本