Propagate exception from _events getter in native EventEmitter

Closed
RO
3mo ago
+101-14

What does this PR do?

jsEventEmitterCastFast in src/jsc/bindings/webcore/JSEventEmitterCustom.cpp reads the _events property on the receiver via getIfPropertyExists, which can run a user getter or Proxy trap. If that getter throws, the code previously fell through with a pending exception, created a fresh EventEmitter wrapper, called putDirect, and then cleared the pending exception via DECLARE_TOP_EXCEPTION_SCOPE + tryClearException. The user's error was silently dropped and each caller then threw its own ERR_INVALID_THIS TypeError.

Repro

const nativeOn = Object.getPrototypeOf(process).on;
const sentinel = new Error("getter threw");
const obj = {};
Object.defineProperty(obj, "_events", { get() { throw sentinel; } });
nativeOn.call(obj, "foo", () => {});
// before: TypeError: Can only call EventEmitter.addListener on instances of EventEmitter
// after:  Error: getter threw

Fix

  • Declare the throw scope before getIfPropertyExists and RETURN_IF_EXCEPTION immediately after it so the exception propagates instead of being cleared. This also removes the DECLARE_TOP_EXCEPTION_SCOPE / tryClearException block and the TODO comment that flagged the problem.
  • Add RETURN_IF_EXCEPTION after every jsEventEmitterCast / jsEventEmitterCastFast call in JSEventEmitter.cpp and in IDLOperation<JSEventEmitter>::call so callers don't overwrite the pending exception with a generic "invalid this" error (VM::throwException replaces rather than preserves a pending exception).

How did you verify your code works?

Added a regression test in test/js/node/events/event-emitter.test.ts covering on, addListener, once, emit, removeListener, removeAllListeners, eventNames, listenerCount, listeners, getMaxListeners, plus a Proxy variant. All 11 cases fail on the current release (received ERR_INVALID_THIS instead of the getter's error) and pass with this change. Also verified the full event-emitter.test.ts suite and the new cases pass under BUN_JSC_validateExceptionChecks=1.