它不报错、不报警、不重启——直到凌晨三点用户投诉全线崩溃。
你是否写过这样的代码?
app.post('/api/notify', (req, res) => {
sendEmail(req.body.email); // 忘记 await,也没 catch
res.status(200).send('OK');
});
async function sendEmail(email) {
await smtpClient.send({ to: email, subject: 'Welcome!' });
}
看起来一切正常?
但只要 smtpClient.send() 抛出异常(比如网络超时、邮箱无效),一个未处理的 Promise 拒绝(Unhandled Rejection)就诞生了。
而在 Node.js 中,这颗“定时炸弹”可能直接导致进程退出——悄无声息,不留痕迹。
为什么 Unhandled Rejection 如此危险?
从 Node.js v15 开始,官方默认行为已改为:
任何未处理的 Promise 拒绝都会导致进程直接退出!
是的,你没看错——不是警告,不是日志,是直接 kill 掉整个服务。
即使你用 PM2、Docker 或 Kubernetes 托管,服务也会不断重启 → 崩溃 → 再重启,形成“死亡循环”。
更可怕的是:
- 错误可能发生在非主流程(如埋点、日志上报、异步通知);
- 用户请求已返回成功(
res.send已调用),你以为“没问题”; - 实际后台任务失败,且无人知晓,直到数据丢失、订单漏发……
真实案例:一封邮件毁掉整站
某电商平台在用户下单后异步发送通知:
orderService.create(order);
sendNotification(order.userId); // 忘记处理异常
某天第三方通知服务宕机,sendNotification 抛出错误。
由于未捕获,Node.js 进程退出。
K8s 自动重启 Pod,但新请求进来又触发同样逻辑 → 全站每分钟崩溃一次。
运维查了两小时日志才发现:根本没有 error 日志!只有进程退出记录。
根源?一个被忽略的 await。
三大常见“漏网之鱼”
场景一:忘记 await 且不 catch
// 危险!fire-and-forget 但未处理拒绝
fireAndForgetTask();
// 正确做法:至少 catch
fireAndForgetTask().catch(err => logger.warn('Task failed', err));
场景二:在 Promise.all 中部分失败
// 只要一个 reject,整个 Promise.all 就 reject
// 如果外层没 catch,就是 unhandled rejection!
await Promise.all([
fetchA(),
fetchB(), // 假设这个失败了
fetchC()
]);
解决方案:用 Promise.allSettled 或单独 catch 每个任务。
场景三:在事件监听器或定时器中抛出异步错误
emitter.on('data', async (d) => {
await process(d); // 如果 process 抛错,没人 catch!
});
这类错误完全脱离主调用栈,极易遗漏。
防御策略:四重保险,杜绝静默崩溃
第一重:全局监听(兜底)
在应用入口添加:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// 发送告警(如 Sentry、企业微信)
// 注意:不要在这里 exit!先记录,再优雅关闭
});
// 同样建议监听 uncaughtException(同步错误)
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
});
全局监听只是“最后防线”,不能替代代码层面的错误处理!
第二重:严格使用 await + try/catch
app.post('/api/notify', async (req, res) => {
try {
await sendEmail(req.body.email);
res.send('OK');
} catch (err) {
logger.error('Send email failed', err);
res.status(500).send('Failed');
}
});
第三重:对“fire-and-forget”任务显式处理
如果确实不需要等待结果(如打点、日志),也要 .catch:
// 明确表示“我知道可能失败,但我选择忽略”
sendAnalytics(event).catch(err => {
// 至少记录,避免 unhandled rejection
logger.debug('Analytics failed (ignored)', err);
});
第四重:ESLint + TypeScript 防呆
配置 ESLint 规则:
{
"rules": {
"require-await": "error",
"no-void": "warn"
}
}
或者用 TypeScript 的 Promise<void> 显式标注,配合 lint 工具提醒未处理的 Promise。
终极心法:所有异步操作,必须有“归宿”
无论是:
- API 调用
- 数据库写入
- 消息队列投递
- 文件读写
只要它返回 Promise,你就必须回答一个问题:
“如果它失败了,谁来负责?”
如果没有答案,那就是隐患。
结语
Node.js 的优雅在于异步非阻塞,
但它的脆弱也藏在每一个被忽略的 reject 里。
别让一个小小的 await 缺失,
毁掉你精心构建的高可用服务。
从今天起,没有“无所谓”的异步调用,只有“已处理”和“待修复”。
转发给你团队里那个总说“异步不用 catch”的人吧!
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!