EventEmitter 的 async 监听器报错,try/catch 接不住
emit返回布尔值,不会等待async监听器。为自己的实例安排拒绝入口和同步error处理,保留同步抛错区别,任务完成需明确Promise接口。
外层用 try/catch 包住 emitter.emit(),同步监听器抛错能被捕获,async 监听器抛错却变成未处理的 Promise rejection。即使给 emit 加 await,也没有等到异步监听器完成:emit 返回的是有没有监听器的布尔值。
先区分同步抛错和监听器返回的 Promise 拒绝。async 函数里的 throw,包括第一次 await 之前的 throw,都会体现在它返回的 Promise 上,不能按普通同步函数解释。
captureRejections 把错误送往另一个入口
支持该选项的 Node.js 中,可以为自己维护的 EventEmitter 实例开启 captureRejections: true。这会为监听器返回的 Promise 安排拒绝处理;默认情况下错误异步送往 error 事件。如果实例定义了 Symbol.for('nodejs.rejection') 方法,则走那个入口。
这个选项在 Node.js 12.16.0、13.4.0 加入,默认关闭。旧环境应先核对支持情况,再按项目兼容要求升级。不能把所有版本下未处理拒绝的终端警告或退出行为都写成相同结果。
下面是普通 CommonJS 演示,错误监听器在触发任务前注册;示例只产生一个受控错误,不连接业务服务:
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter({ captureRejections: true });
emitter.on('error', (error) => {
console.error('task failed:', error.message);
});
emitter.on('task', async () => {
throw new Error('demo failure');
});
emitter.emit('task');
这里错误通过 error 监听器处理,不是重新让外层 emit 的 try/catch 捕获。真实任务要按业务决定失败后停机、记录失败状态还是继续;日志只展示错误入口,不能用来代替任务失败的处理。
没有注册 error 监听器却发出 error,会使错误被抛出。只设置 captureRejections 不够,也不要依赖 errorMonitor 作为消费错误的替代:它用于观察,不会自动阻止未处理 error 的行为。
同步错误仍按同步调用处理
captureRejections 针对监听器返回的 Promise。普通同步监听器直接 throw,并不会因此自动被转换成 error 事件。应用若同时存在同步监听器,仍需按原来的调用契约处理它们的同步错误。
不要把 error 监听器也写成 async,再期待同一机制处理它自己的拒绝。为避免错误循环,captureRejections 产生的 error 处理不会再次进行同样的拒绝捕获。确实需要异步记录或通知时,应给那个异步操作安排独立的失败处理,并明确其结果不会替原业务恢复成功。
只需在自己的监听器里处理错误时,也可在其内部 await 实际任务并用 try/catch 接住失败。核对该监听器是否等待了所有需要等待的操作;另起一个 Promise 却既不返回也不捕获,仍可能遗漏拒绝。
调用方需要完成结果时,重新明确任务接口
await emit 得到的仍是布尔值。captureRejections 也没有让 emit 变成“所有监听器已完成”的 Promise。如果调用方必须依据成功结果继续,采用明确返回 Promise 的任务函数,并让调用方等待它。事件可以用于通知,但不能凭一次 emit 的返回值确认业务完成。
避免为单个模块修改全局 captureRejections 默认值;优先把范围限定在自己拥有的实例,并检查已有自定义拒绝处理器。
验收时分别触发同步错误、异步拒绝和正常完成,核对业务状态、错误入口及调用方继续执行的时机。多监听器任务还要判断失败时其他操作是否已经开始,不能因捕获到一次错误就整批重跑。恢复原监听配置也不会撤销已经发生的副作用。


