Replies: 3 comments 1 reply
|
I think you're confusing concepts although your description isn't clear enough that I can pinpoint it exactly. Event emitters are synchronous, async functions are not. If you mix them without being mindful of that, then yes, it's going to sting. Not a bug though. I'm going to convert this to a discussion. |
|
Event emitters are synchronous, meaning calling To fix it, process events sequentially using an async loop or custom emitter that |
|
I think the behavior here is expected once the listener is an async function. EventEmitter.emit() does invoke listeners synchronously, but it does not await their return values. emit() itself only returns a boolean indicating whether the event had listeners. The important part is that an async function always returns a Promise. A throw inside an async listener therefore becomes a rejection of that Promise, even if the throw happens before the first await. So conceptually: async function listener() { does not expose "boom" as a synchronous exception to the caller of listener(). It gives the caller a rejected Promise. That means a try/catch surrounding: emitter.emit("message"); cannot catch that rejection, because emit() does not return or await the Promise returned by the listener. captureRejections is specifically designed to bridge this gap. Node attaches a rejection handler to Promises returned by listeners and routes rejected Promises to Symbol.for("nodejs.rejection") or the "error" event. That necessarily happens through Promise rejection handling, so it cannot behave like a synchronous throw from emit(). If the caller needs to wait for every handler and directly observe their errors, EventEmitter may not be the right abstraction for that particular operation. An explicit async dispatch method could make that contract clearer, for example by collecting the listeners' return values and awaiting them. Something conceptually like: async function emitAsync(emitter, event, ...args) { Then: try { For normal fire-and-forget events, captureRejections seems like the appropriate mechanism. For events where completion and error propagation are part of the API contract, I would use an explicitly asynchronous API instead of expecting EventEmitter.emit() to propagate Promise rejections synchronously. So I don't think Node can synchronously catch the initial throw from an async listener without changing the semantics of async functions or the EventEmitter API itself. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Version
20.13.1, probably all others too.
Platform
Subsystem
No response
What steps will reproduce the bug?
When an async function throws an error during an event emitter callback, the error is not caught by catch block at the time of execution its event but only after all events are done. This behavior:
Reproduction:
How often does it reproduce? Is there a required condition?
Always. No special Condition.
What is the expected behavior? Why is that the expected behavior?
All errors thrown in async functions called from event handlers should be catchable by surrounding try/catch blocks at the time of the function call.
What do you see instead?
All function calls first get executed and only then all the catch blocks get executed. (Immagine what would happen with some complex Error Handling)
Additional information
Impact: This affects all event emitter usage with async handlers (MQTT clients, OPCUA subscriptions,...)
Maybe this is technically correct per the spec, but creates dangerous real-world behavior, if one wants to handle all exceptions in an ExceptionHandler outside from the called async function.
All reactions