异步编程让程序不用阻塞等待,但也让错误的传播路径变得复杂。回调函数的错误处理靠约定(通常第一个参数是错误),Promise 把错误传递规范化成了一条链,而 async/await 又把异步错误“拉回”了同步的 try/catch 写法。理解每一种机制的特性和边界,才能写出健壮的异步代码。
Promise 链中的错误捕获
Promise 内部的错误(无论是 reject 调用还是 throw)都会被沿着链向下传递,直到遇到第一个 .catch()。正因为这样,.catch() 放在链末尾可以统一捕获前面任意环节的异常。
fetchUserData()
.then(parseResponse)
.then(updateUI)
.catch(error => {
// 捕获前面任何一个 then 抛出的错误
console.error('操作失败:', error);
});
一些容易掉进去的陷阱:
- 中间 catch 后又继续 then:
如果 .catch() 返回了普通值(或者不返回),后面的 .then() 仍然会执行,并且认为错误已经“修复”了。需要明确链的分支意图,确保错误不会在无意中被吞掉。
- 忘记 return:
在 .then() 的回调里没有 return Promise,会导致链中的错误无法被后续 .catch() 捕获,出现 UnhandledPromiseRejection。
- 在 Promise 构造函数里忘记 reject:
手动写 new Promise((resolve, reject) => { ... }) 时,必须确保所有错误路径都能调用 reject,否则 Promise 会永远挂起,外部根本无法捕获错误。
async/await 中的错误捕获
async 函数始终返回 Promise,所以错误依然是 Promise rejection。但与 Promise 链不同,async 函数的错误可以用同步的 try/catch 来包裹。
async function loadPage() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
render(user, posts);
} catch (error) {
console.error('页面加载失败:', error);
showErrorPage();
}
}
这里需要注意几点:
- await 后面的异步操作抛出异常,catch 可以捕获,因为 await 会把 rejected Promise 转换成一个普通的 throw。
- 并发请求需要分别处理:如果一次性 await 多个 Promise,且没有用
Promise.all包裹,那么其中一个失败就会导致整个 try 块跳到 catch,其他请求可能被忽略。更推荐的做法是:
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
- 循环中使用 await 的问题:在循环体里逐个 await 操作,一旦其中一个失败,循环终止,后续操作全部跳过。通常应当用
Promise.allSettled或提前收集错误,继续执行可以安全跳过的操作。
全局未捕获的 Promise 错误
在浏览器中,如果一个 Promise 被 reject 了,且没有 .catch() 或 try/catch 处理,会触发 unhandledrejection 事件。在 Node.js 中则会打印警告并可能导致进程退出。添加全局监听可以兜底收集这些遗漏的错误:
// 浏览器
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 拒绝:', event.reason);
// 可以上报到错误监控平台
});
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});
这条线是最后一道防线,但不该替代业务代码中合理的错误处理逻辑。它更适合作为监控警告,提醒开发者哪里遗漏了 catch。
实际项目中的最佳实践
- 永远不要让错误“沉默”
空的 .catch() 或者 catch 后什么都不做,等于彻底把错误埋掉。至少要记录日志,或者让错误以某种形式反馈给调用者。
- 区分可恢复错误与致命错误
网络超时、数据格式错误通常可以降级处理(展示占位图、重试);而鉴权失败、关键数据缺失等则需要中断流程并提示用户。
- 统一的错误处理中间件
在 Node.js 服务端,可以设计一个全局错误处理中间件;在前端,可以在请求库(如 Axios)的拦截器中统一处理 HTTP 错误,避免在每个组件里重复写 catch。
- 将错误信息结构化
不要只抛出一个字符串,抛出一个 Error 对象(或包含 code、message、detail 的对象),便于错误上报后快速定位问题。
- 限制重试和超时
重试逻辑要设置最大尝试次数和间隔,超时控制避免异步操作永久挂起。很多错误处理工具(如 p-retry)已经标准化了这些模式。
- 测试异步错误行为
单元测试中不仅要测正常路径,还要通过模拟网络断连、数据异常等场景,确保错误处理分支确实被覆盖。
异步错误处理并不神秘。把 Promise 的 reject 看作一种“未来可能发生的异常”,然后用你熟悉的 try/catch(或 catch 方法)去规划异常流。当开发者养成“每条异步路径都有归宿”的习惯时,绝大多数异步 bug 就可以在代码审查阶段被杜绝。