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', () => {