Skip to content

Commit bc09d18

Browse files
myieyeclaude
andcommitted
Normalize error-toast title in unifyErrorEvent
Derive the display title once in unifyErrorEvent — Error.toString(), which keeps the error type (e.g. "TypeError: …") and drops the browser's "Uncaught" prefix — instead of re-deriving it in processErrorIntoDetails. Consistent across thrown Errors, ErrorEvents, and promise rejections, and it keeps the type visible (the earlier event.error.message dropped it entirely once the stack header was stripped from the detail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ffaf197 commit bc09d18

2 files changed

Lines changed: 43 additions & 11 deletions

File tree

frontend/viewer/src/lib/errors/global-errors.test.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {describe, expect, it} from 'vitest';
22

3-
import {processErrorIntoDetails} from './global-errors';
3+
import {processErrorIntoDetails, unifyErrorEvent} from './global-errors';
44

55
describe('processErrorIntoDetails', () => {
66
it('splits a .NET error at the first stack frame', () => {
@@ -38,13 +38,43 @@ describe('processErrorIntoDetails', () => {
3838
expect(detail).toBeUndefined();
3939
});
4040

41-
it('does not repeat a JS Error message in both title and detail', () => {
41+
it('shows frames-only detail for a JS error, not repeating the title', () => {
4242
const error = new Error('boom');
4343

44-
const {message: title, detail} = processErrorIntoDetails({message: 'Uncaught Error: boom', error});
44+
const {message: title, detail} = processErrorIntoDetails(unifyErrorEvent(error));
4545

46-
expect(title).toBe('boom');
46+
expect(title).toBe('Error: boom');
4747
expect(detail).not.toContain('boom');
4848
expect(detail).toContain('at ');
4949
});
5050
});
51+
52+
describe('unifyErrorEvent', () => {
53+
it('uses Error.toString() (type kept, no "Uncaught" prefix) for an ErrorEvent with an error', () => {
54+
const error = new TypeError('x is not a function');
55+
const event = {message: 'Uncaught TypeError: x is not a function', error, filename: 'f', lineno: 1, colno: 2};
56+
57+
const unified = unifyErrorEvent(event as unknown as ErrorEvent);
58+
59+
expect(unified.message).toBe('TypeError: x is not a function');
60+
expect(unified.error).toBe(error);
61+
});
62+
63+
it('uses Error.toString() for a rejected Error reason', () => {
64+
const error = new TypeError('x is not a function');
65+
66+
const unified = unifyErrorEvent({reason: error} as unknown as PromiseRejectionEvent);
67+
68+
expect(unified.message).toBe('TypeError: x is not a function');
69+
expect(unified.error).toBe(error);
70+
});
71+
72+
it('falls back to the event message for an ErrorEvent without an error object', () => {
73+
const event = {message: 'Script error.', error: null, filename: '', lineno: 0, colno: 0};
74+
75+
const unified = unifyErrorEvent(event as unknown as ErrorEvent);
76+
77+
expect(unified.message).toBe('Script error.');
78+
expect(unified.error).toBeNull();
79+
});
80+
});

frontend/viewer/src/lib/errors/global-errors.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ type UnifiedErrorEvent = {
1010
at?: string;
1111
}
1212

13-
function unifyErrorEvent(event: ErrorEvent | PromiseRejectionEvent | Error): UnifiedErrorEvent {
13+
// message is normalized here to a display-ready title so consumers don't each re-derive one. For any Error we
14+
// use toString() ("TypeError: x", not the bare "x") to keep the type, and drop the browser's "Uncaught" prefix.
15+
export function unifyErrorEvent(event: ErrorEvent | PromiseRejectionEvent | Error): UnifiedErrorEvent {
1416
if (event instanceof Error) {
15-
return { message: event.message, error: event };
17+
return { message: event.toString(), error: event };
1618
} else if ('message' in event) {
17-
return { message: event.message, error: event.error, at: `${event.filename}:${event.lineno}:${event.colno}` };
19+
return { message: event.error instanceof Error ? event.error.toString() : event.message, error: event.error, at: `${event.filename}:${event.lineno}:${event.colno}` };
1820
} else if (typeof event.reason === 'string') {
1921
return { message: event.reason, error: null };
2022
} else if (event.reason instanceof Error) {
21-
return { message: event.reason.message, error: event.reason };
23+
return { message: event.reason.toString(), error: event.reason };
2224
} else {
2325
return { message: 'Unknown error', error: event.reason };
2426
}
@@ -50,9 +52,9 @@ export function processErrorIntoDetails(event: UnifiedErrorEvent): {message: str
5052
const message = event.message;
5153
const match = dotnetErrorRegex.exec(message);
5254
if (match) return {message: match[1].trim(), detail: message.substring(match[1].length).trim()};
53-
// A JS Error's stack repeats the message on its first line(s); use the prefix-free error message as the
54-
// title and just the frames as the detail, so the message isn't shown in both slots.
55-
else if (event.error instanceof Error) return {message: event.error.message || message, detail: stackFrames(event.error)};
55+
// message is already the display title (see unifyErrorEvent); attach just the stack frames as detail —
56+
// stackFrames drops the leading "Error: <message>" header so the message isn't shown in both slots.
57+
else if (event.error instanceof Error) return {message, detail: stackFrames(event.error)};
5658
else return {message};
5759
}
5860

0 commit comments

Comments
 (0)