Propagate JS exceptions instead of clearing them where the caller is fallible

Merged
DY
3mo ago
+416-298

What does this PR do?

A number of native paths cleared a pending JavaScript exception and carried on with a default value even though the function above them already returns JsResult / checks a ThrowScope. The clear defeated an existing error path, so the error was silently lost. Each site now leaves the exception pending and returns early. This is the "trivial" subset of a wider audit of exception-clearing in the runtime: only sites where no signature or FFI contract had to change.

User-visible effects:

  • mock(fn) / spyOn: a throwing name/length getter on the implementation throws instead of being ignored.

  • mockFn.withImplementation(impl, cb): a sync throw from cb propagates (was swallowed). Separately, the sync-return path now restores the queued mockImplementationOnce tail correctly (it restored the head, dropping queued once-impls).

  • mock.module: a factory export getter that throws fails the import / the mock.module call instead of exporting undefined; an already-imported namespace is left untouched (exports are read before any is overridden).

  • Native EventEmitter prototype methods (e.g. Object.getPrototypeOf(process).on) called on a foreign this whose _events getter throws now propagate that error instead of ERR_INVALID_THIS.

  • ESM import of a CJS module whose module.exports can't be enumerated (throwing ownKeys) rejects instead of yielding an empty namespace.

  • expect(fn).toThrow(/re/): if matching the message throws, that error propagates instead of being used as the match result.

  • Bun.inspect: a throwing toJSON (URLSearchParams/Headers/FormData) or Proxy getPrototypeOf trap propagates, matching util.inspect.

  • ServerWebSocket.cork(cb): a returned Error is returned, not thrown.

  • generateKeyPair('rsa-pss', { hash }): an error while emitting DEP0154 propagates.

  • Module._findPath: an error thrown by a Bun.plugin onResolve hook propagates (as require.resolve already does) instead of the path being skipped.

  • setTimeout/setInterval with an out-of-range delay: an error thrown while emitting the warning propagates (as in Node).

  • X509Certificate with an empty subject/issuer DN: toLegacyObject() (and the .subject/.issuer internals) return undefined/{} like Node instead of throwing "Crypto operation failed" — the getters used to throw-and-swallow it, toLegacyObject() threw it.

Also propagated (OOM/termination-only, no repro): bun:test/object-loader export enumeration, native ReadableStream cancel/error, napi_create_*error, util.types.isError tag read, addEventListener(type, null) warning, Windows IPC socket payload attach.

Deliberately not in this PR: the static-table PropertyCallback builders (process.stdout & co., Module._cache) keep their current clear+report. Propagating from those needs the builders to stop entering JS (checked custom getters) rather than teaching JSC's own-property lookup to throw; that is a follow-up.

How did you verify your code works?

Added tests next to existing coverage for each user-visible case; each fails with USE_SYSTEM_BUN=1 and passes on the debug build. Ran the repros under BUN_JSC_validateExceptionChecks=1.