From a7242c936d5e7a7357caa2114b3689e9270532ac Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:50:38 +0000 Subject: [PATCH 1/3] bun:test: propagate the exception thrown while formatting a snapshot value match_and_fmt_snapshot discarded the error from jest_snapshot_pretty_format and threw a new "Failed to pretty format value: ..." error instead. The original exception was still pending at that point, so rendering the value for the new message failed as well and create_error_instance cleared the pending exception, leaving the matcher throwing an Error whose message ends right after the colon. Every Err from jest_snapshot_pretty_format already has the real exception pending (or is OutOfMemory/Terminated, which the host function wrapper handles), so return it as is: the getter's own error now comes out of toMatchSnapshot / toMatchInlineSnapshot. --- src/runtime/test_runner/expect.rs | 9 +- src/runtime/test_runner/mod.rs | 6 +- .../test/snapshot-tests/bun-snapshots.test.ts | 102 ++++++++++++++++++ 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index cf95b89cca37..38abd1661d4b 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1182,14 +1182,7 @@ impl Expect { } } - if value.jest_snapshot_pretty_format(pretty_value, global_this).is_err() { - let mut formatter = ConsoleObject::Formatter::new(global_this); - return Err(global_this.throw(format_args!( - "Failed to pretty format value: {}", - value.to_fmt(&mut formatter), - ))); - } - Ok(()) + value.jest_snapshot_pretty_format(pretty_value, global_this) } pub(crate) fn snapshot( diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 599b30669b38..1e3e244bde07 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -275,9 +275,9 @@ pub mod expect { )?; // `FormatOptions.flush` is false, so the formatter does not flush // internally; a buffered `out` would otherwise drop trailing - // snapshot bytes. Propagate the writer error as a thrown JS error - // so the caller's `.is_err()` branch - // (expect.rs `to_match_snapshot_value_kind`) fires. + // snapshot bytes. Every `Err` out of this function has a JS + // exception pending (the formatter throws before failing), so the + // writer error is thrown here too and callers can simply `?` it. out.flush().map_err(|e| global.throw_error(e, "snapshot writer flush failed"))?; Ok(()) } diff --git a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts index e670e343921f..3a0ec80066b7 100644 --- a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts +++ b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts @@ -61,4 +61,106 @@ describe("toMatchSnapshot errors", () => { expect({ a: 4 }).toMatchSnapshot({ a: expect.any("not a constructor") }); }).toThrow(); }); + + describe("when formatting the received value throws", () => { + // The snapshot formatter reads `$$typeof` off every object (React element + // detection), `size` off Maps and Sets, and JSON-stringifies Dates, so + // user code on any of those runs while the value is being formatted. + const received: [string, () => unknown][] = [ + [ + "$$typeof getter on the received value", + () => ({ + get $$typeof(): unknown { + throw new Error("boom"); + }, + }), + ], + [ + "$$typeof getter on a nested value", + () => ({ + a: { + get $$typeof(): unknown { + throw new Error("boom"); + }, + }, + }), + ], + [ + "size getter on a Map", + () => + Object.defineProperty(new Map(), "size", { + get() { + throw new Error("boom"); + }, + }), + ], + [ + "size getter on a Set", + () => + Object.defineProperty(new Set(), "size", { + get() { + throw new Error("boom"); + }, + }), + ], + [ + "toJSON on a Date", + () => + Object.assign(new Date(0), { + toJSON() { + throw new Error("boom"); + }, + }), + ], + ]; + + it.each(received)("toMatchSnapshot throws the error from the %s", (_, makeValue) => { + expect(() => expect(makeValue()).toMatchSnapshot()).toThrow("boom"); + }); + + it.each(received)("toMatchInlineSnapshot throws the error from the %s", (_, makeValue) => { + // Passing the inline snapshot means a build that does not throw fails on + // the mismatch instead of writing into this file. + expect(() => expect(makeValue()).toMatchInlineSnapshot(`"never recorded"`)).toThrow("boom"); + }); + + it("throws the exception itself rather than a wrapper", () => { + const error = new Error("boom"); + const value = { + get $$typeof(): unknown { + throw error; + }, + }; + + let thrown: unknown; + try { + expect(value).toMatchSnapshot(); + } catch (e) { + thrown = e; + } + expect(thrown).toBe(error); + + thrown = undefined; + try { + expect(value).toMatchInlineSnapshot(`"never recorded"`); + } catch (e) { + thrown = e; + } + expect(thrown).toBe(error); + }); + + it("still throws the formatting error after the property matchers matched", () => { + // A fresh object per call: matching property matchers writes them into the received object. + const makeValue = () => ({ + n: 1, + get $$typeof(): unknown { + throw new Error("boom"); + }, + }); + expect(() => expect(makeValue()).toMatchSnapshot({ n: expect.any(Number) })).toThrow("boom"); + expect(() => expect(makeValue()).toMatchInlineSnapshot({ n: expect.any(Number) }, `"never recorded"`)).toThrow( + "boom", + ); + }); + }); }); From 96f7655c5aac943cd32c404a95242e3715d5c049 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:03:12 +0000 Subject: [PATCH 2/3] Point the test workarounds at their issues; describe the flush accurately --- src/runtime/test_runner/mod.rs | 9 ++++----- test/js/bun/test/snapshot-tests/bun-snapshots.test.ts | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 1e3e244bde07..42fdf23664a2 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -273,11 +273,10 @@ pub mod expect { out, fmt_options, )?; - // `FormatOptions.flush` is false, so the formatter does not flush - // internally; a buffered `out` would otherwise drop trailing - // snapshot bytes. Every `Err` out of this function has a JS - // exception pending (the formatter throws before failing), so the - // writer error is thrown here too and callers can simply `?` it. + // The formatter ignores the result of its own flush. Throw the + // writer error here so that, like the formatter's failures, every + // `Err` from this function has a JS exception pending and callers + // can simply `?` it. out.flush().map_err(|e| global.throw_error(e, "snapshot writer flush failed"))?; Ok(()) } diff --git a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts index 3a0ec80066b7..c4cfae4255b9 100644 --- a/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts +++ b/test/js/bun/test/snapshot-tests/bun-snapshots.test.ts @@ -76,6 +76,8 @@ describe("toMatchSnapshot errors", () => { }), ], [ + // Keep this a single property: until #37331 lands, the property walk + // only surfaces an exception thrown while formatting the last key. "$$typeof getter on a nested value", () => ({ a: { @@ -150,7 +152,7 @@ describe("toMatchSnapshot errors", () => { }); it("still throws the formatting error after the property matchers matched", () => { - // A fresh object per call: matching property matchers writes them into the received object. + // Fresh object per call: matched property matchers are written into the received object (#3521). const makeValue = () => ({ n: 1, get $$typeof(): unknown { From a79f8cfd1c05a5ce06034e94d9bdc9281d80df22 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:05:31 +0000 Subject: [PATCH 3/3] Trim the flush comment to one line --- src/runtime/test_runner/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/runtime/test_runner/mod.rs b/src/runtime/test_runner/mod.rs index 42fdf23664a2..f7074a615bd0 100644 --- a/src/runtime/test_runner/mod.rs +++ b/src/runtime/test_runner/mod.rs @@ -273,10 +273,7 @@ pub mod expect { out, fmt_options, )?; - // The formatter ignores the result of its own flush. Throw the - // writer error here so that, like the formatter's failures, every - // `Err` from this function has a JS exception pending and callers - // can simply `?` it. + // The formatter ignores its own flush result; a writer error has to be thrown like any other failure. out.flush().map_err(|e| global.throw_error(e, "snapshot writer flush failed"))?; Ok(()) }