From 6c65c85c2c90d1968927e4f58ad055e4fe5a914f Mon Sep 17 00:00:00 2001 From: Ryan McGee <137445630+rjmcgee53192@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:44:00 -0500 Subject: [PATCH] fix(core): stop normalize() from throwing on symbol and null-prototype errors normalize() built its **non-serializable** marker by interpolating the caught value into a template literal. That conversion throws for a symbol and for an object with no usable toString, so a property getter throwing either one made normalize() throw instead of returning the marker its docs promise. Convert the caught value with String() inside a try/catch and fall back to fixed text when even that throws. Covers both catch blocks in normalize.ts (normalize and stringifyValue) and adds regression tests for a getter throwing a symbol, a getter throwing a null-prototype object, and stringifyValue failing with a symbol. Fixes #25920 --- packages/core/src/utils/normalize.ts | 18 ++++++++-- .../core/test/lib/utils/normalize.test.ts | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/normalize.ts b/packages/core/src/utils/normalize.ts index 0a31a0c53885..059f69b44892 100644 --- a/packages/core/src/utils/normalize.ts +++ b/packages/core/src/utils/normalize.ts @@ -57,7 +57,21 @@ export function normalize(input: unknown, depth: number = 100, maxProperties: nu // since we're at the outermost level, we don't provide a key return visit('', input, depth, maxProperties); } catch (err) { - return { ERROR: `**non-serializable** (${err})` }; + return { ERROR: `**non-serializable** (${stringifyCaughtValue(err)})` }; + } +} + +/** + * Stringify a caught value for an error message. Template literals and other implicit + * conversions throw on symbols and on objects with no usable `toString`, which is how + * the values that end up here tend to be broken in the first place, so fall back to + * fixed text instead of throwing again. + */ +function stringifyCaughtValue(err: unknown): string { + try { + return String(err); + } catch { + return 'unknown error'; } } @@ -240,7 +254,7 @@ export function stringifyValue( return `[object ${objName}]`; } catch (err) { - return `**non-serializable** (${err})`; + return `**non-serializable** (${stringifyCaughtValue(err)})`; } } /* eslint-enable complexity */ diff --git a/packages/core/test/lib/utils/normalize.test.ts b/packages/core/test/lib/utils/normalize.test.ts index 772abec88525..68a7e8e07979 100644 --- a/packages/core/test/lib/utils/normalize.test.ts +++ b/packages/core/test/lib/utils/normalize.test.ts @@ -588,6 +588,40 @@ describe('normalize()', () => { someFunc: '**non-serializable** (Error: Nope)', }); }); + + test('does not throw when a getter throws a symbol', () => { + const obj = { + get bad(): unknown { + throw Symbol('s'); + }, + }; + + expect(normalize({ bad: obj }, 3)).toEqual({ + ERROR: '**non-serializable** (Symbol(s))', + }); + }); + + test('does not throw when a getter throws a null-prototype object', () => { + const obj = { + get bad(): unknown { + throw Object.create(null); + }, + }; + + expect(normalize({ bad: obj }, 3)).toEqual({ + ERROR: '**non-serializable** (unknown error)', + }); + }); + + test('does not throw when stringifying a value fails with a symbol', () => { + vi.spyOn(stacktraceModule, 'getFunctionName').mockImplementationOnce(() => { + throw Symbol('s'); + }); + + expect(normalize({ someFunc: () => {} })).toEqual({ + someFunc: '**non-serializable** (Symbol(s))', + }); + }); }); test('runs registered stringifier for each visited value', () => {