diff --git a/src/emitters/ReadonlyEmitterBase.ts b/src/emitters/ReadonlyEmitterBase.ts index 5df1844..b9fc77a 100644 --- a/src/emitters/ReadonlyEmitterBase.ts +++ b/src/emitters/ReadonlyEmitterBase.ts @@ -82,9 +82,9 @@ export abstract class ReadonlyEmitterBase implements ReadonlyEmitter(type: K | '*', listener: Function) { const onceListener = ((event: Event) => { this.off(type, onceListener); - listener(event); + return listener(event); }) as Function & { [ONCE]?: true }; - + onceListener.toString = listener.toString.bind(listener); onceListener[ONCE] = true as const; return onceListener; } diff --git a/src/emitters/SerialAsyncEmitter.test.ts b/src/emitters/SerialAsyncEmitter.test.ts index 6825b4e..11e9b4f 100644 --- a/src/emitters/SerialAsyncEmitter.test.ts +++ b/src/emitters/SerialAsyncEmitter.test.ts @@ -57,6 +57,16 @@ describe('SerialAsyncEmitter', () => { await emitter.emit('test', { type: 'test', payload: 42 }); expect(listener2).toHaveBeenCalledTimes(1); }); + + it('should tolerate errors in listeners', async () => { + const emitter = new SerialAsyncEmitter('test-emitter'); + const listener1 = jest.fn().mockRejectedValue(new Error('This listener failed')); + const listener2 = jest.fn(); + emitter.once('test', listener1); + emitter.once('test', listener2); + await emitter.emit('test', { type: 'test', payload: 42 }); + expect(listener2).toHaveBeenCalledTimes(1); + }); }); type TestEventMap = { diff --git a/src/emitters/SerialAsyncEmitter.ts b/src/emitters/SerialAsyncEmitter.ts index 06301fd..afa5374 100644 --- a/src/emitters/SerialAsyncEmitter.ts +++ b/src/emitters/SerialAsyncEmitter.ts @@ -1,4 +1,5 @@ import type { AsyncEmitter } from './AsyncEmitter'; +import { logError } from './logError'; import { ReadonlyEmitterBase } from './ReadonlyEmitterBase'; import { __EMIT, __INVOKE } from './syncEmitterCommons'; @@ -18,11 +19,16 @@ export class SerialAsyncEmitter async #doEmit(eventType: K, event: EventMap[K]) { const listeners = [...this._getListeners(eventType)]; + const $eventType = String(eventType); - await this._log.trace.complete(__EMIT(event), String(eventType), async () => { + await this._log.trace.complete(__EMIT(event), $eventType, async () => { if (listeners) { for (const listener of listeners) { - await this._log.trace.complete(__INVOKE(listener), 'invoke', () => listener(event)); + try { + await this._log.trace.complete(__INVOKE(listener), 'invoke', () => listener(event)); + } catch (error: unknown) { + logError(error, $eventType, listener); + } } } }); diff --git a/src/emitters/SerialSyncEmitter.test.ts b/src/emitters/SerialSyncEmitter.test.ts index 634b885..fce63c6 100644 --- a/src/emitters/SerialSyncEmitter.test.ts +++ b/src/emitters/SerialSyncEmitter.test.ts @@ -59,6 +59,18 @@ describe('SerialSyncEmitter', () => { expect(listener2.mock.calls[0][0]).toEqual({ type: 'test', payload: 42 }); expect(listener2.mock.calls[1][0]).toEqual({ type: 'test', payload: 84 }); }); + + it('should tolerate errors in listeners', () => { + const emitter = new SerialSyncEmitter('test-emitter'); + const listener1 = jest.fn(() => { + throw new Error('This listener failed'); + }); + const listener2 = jest.fn(); + emitter.once('test', listener1); + emitter.once('test', listener2); + expect(() => emitter.emit('test', { type: 'test', payload: 42 })).not.toThrow(); + expect(listener2).toHaveBeenCalledTimes(1); + }); }); type TestEventMap = { diff --git a/src/emitters/SerialSyncEmitter.ts b/src/emitters/SerialSyncEmitter.ts index 5a0c11c..eaae766 100644 --- a/src/emitters/SerialSyncEmitter.ts +++ b/src/emitters/SerialSyncEmitter.ts @@ -1,4 +1,5 @@ import type { Emitter } from './Emitter'; +import { logError } from './logError'; import { ReadonlyEmitterBase } from './ReadonlyEmitterBase'; import { __EMIT, __ENQUEUE, __INVOKE } from './syncEmitterCommons'; @@ -24,11 +25,16 @@ export class SerialSyncEmitter while (this.#queue.length > 0) { const [eventType, event] = this.#queue[0]; const listeners = [...this._getListeners(eventType)]; + const $eventType = String(eventType); - this._log.trace.complete(__EMIT(event), String(eventType), () => { + this._log.trace.complete(__EMIT(event), $eventType, () => { if (listeners) { for (const listener of listeners) { - this._log.trace.complete(__INVOKE(listener), 'invoke', () => listener(event)); + try { + this._log.trace.complete(__INVOKE(listener), 'invoke', () => listener(event)); + } catch (error: unknown) { + logError(error, $eventType, listener); + } } } }); diff --git a/src/emitters/logError.ts b/src/emitters/logError.ts new file mode 100644 index 0000000..7ef188f --- /dev/null +++ b/src/emitters/logError.ts @@ -0,0 +1,9 @@ +import { logger } from '../utils'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export function logError(error: unknown, eventType: string, listener: Function) { + const errorDetails = (error instanceof Error && error.stack) || String(error); + logger.warn( + `Caught an error while emitting "${eventType}" event:\n${errorDetails}\nThe listener function was:\n${listener}`, + ); +}