async/await 语法本身并不内置“异常注入”功能——它仅仅是 Promise 的语法糖,让异步代码更简洁。而“异常注入”的核心思想,是在异步执行过程中主动构造并传递带有丰富上下文(如 traceId、操作名称、请求参数等)的错误信息,使得上层的 try/catch 能够捕获到结构化的错误对象,而非仅仅将原始异常抛出。

那么,在实际的 async/await 开发中,如何实现这种“增强型”的异常注入呢?以下是几种经过验证的有效实践。
在 await 前主动 throw 错误
方法一:在 await 执行前主动抛出错误。最直接的方式是在调用 await 之前,根据业务逻辑主动 throw 异常,使流程立即进入 catch 分支。此方法特别适用于参数校验、权限判断、前置状态检查等场景。一旦抛出错误,外层 try/catch 会立即捕获,并且 stack trace 会精确指向当前函数的位置,便于调试。例如:if (!userId) throw new Error('Missing userId');——虽然简单,但非常实用。
封装异步调用并重抛增强型错误
方法二:封装异步调用并重新抛出增强型错误。避免直接使用裸写的 await fetch() 或 await db.query()。建议编写一个工具函数,将异步调用统一封装,在 catch 中构造带有上下文信息的错误对象。这样做既能保留原始 error.stack,又能附加当前层的额外信息(如 URL、SQL 语句、traceId)。推荐使用自定义错误类(如 AppError),包含 code、context、cause 等字段,使错误信息更加丰富。例如:throw new AppError('DB_TIMEOUT', { sql, traceId }, originalErr);——这样逐层传递,每个环节都能追加自己的上下文,调用链清晰可追溯。
在 Promise 构造器中 reject 自定义错误
方法三:在 Promise 构造器内 reject 自定义错误。当需要手动创建 Promise(例如封装回调 API、定时器、事件监听)时,可以在 reject 时传入结构化的错误对象。但需注意:必须确保该 Promise 被 await 处理,否则 reject 不会进入 catch 分支。此外,切忌直接 reject 一个字符串——应传递 Error 实例,以保留 stack trace 和类型信息。例如:reject(new AppError('TIMEOUT', { timeout: 5000 }));——清晰、完整、易于追踪。
避免常见陷阱
以下几种错误处理方式会导致异常被静默吞噬,或使调试变得异常困难:
- 调用 async 函数时既不加 await,也不调用
.catch()——异常会变成未处理的 rejection(unhandled rejection),try/catch 无法捕获。 - 在 catch 中仅执行
console.error而不重新抛出(re-throw),也不返回明确的降级值——错误被静默忽略,后续逻辑误以为一切正常。 - 使用全局变量存储 traceId 或错误上下文——多请求并发时,变量会被互相覆盖,导致上下文混淆,调试信息完全错误。
- 拼接错误栈时丢弃原始 error.cause,或不保留
new Error().stack——调用链断裂,排查问题几乎只能靠猜测。
