Propagate exception from _events getter in native EventEmitter
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
getIfPropertyExistsandRETURN_IF_EXCEPTIONimmediately after it so the exception propagates instead of being cleared. This also removes theDECLARE_TOP_EXCEPTION_SCOPE/tryClearExceptionblock and theTODOcomment that flagged the problem. - Add
RETURN_IF_EXCEPTIONafter everyjsEventEmitterCast/jsEventEmitterCastFastcall inJSEventEmitter.cppand inIDLOperation<JSEventEmitter>::callso callers don't overwrite the pending exception with a generic "invalid this" error (VM::throwExceptionreplaces 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.