diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 81d0ec710ed2..5caeae386a32 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5048,6 +5048,8 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: prototypeCount = 1; } } + // Ignore exceptions from Proxy "getPrototype" trap. + CLEAR_IF_EXCEPTION(scope); } } auto* propertyNames = vm.propertyNames; @@ -5238,7 +5240,12 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC: break; if (iterating == globalObject) break; - iterating = iterating->getPrototype(globalObject).getObject(); + JSValue proto = iterating->getPrototype(globalObject); + if (scope.exception()) [[unlikely]] { + (void)scope.tryClearException(); + break; + } + iterating = proto.getObject(); } } diff --git a/test/js/bun/test/expect-proxy-prototype-crash.test.ts b/test/js/bun/test/expect-proxy-prototype-crash.test.ts new file mode 100644 index 000000000000..957248dc154f --- /dev/null +++ b/test/js/bun/test/expect-proxy-prototype-crash.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// Regression test: formatting an Expect object whose prototype has been +// replaced with a Proxy should not crash when toBe() fails and the error +// message formatter walks the prototype chain. +test("expect error formatting does not crash with Proxy prototype", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const v1 = Bun.jest(); +const v2 = v1.expect(v1); +Object.setPrototypeOf(v2, new Proxy(Object.getPrototypeOf(v2), {})); +try { v2.toBe(v2); } catch (e) {} +console.log("OK");`, + ], + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("OK\n"); + expect(exitCode).toBe(0); +});