diff --git a/frontend/src/errorReporting.ts b/frontend/src/errorReporting.ts index eb9ea66..2a907ee 100644 --- a/frontend/src/errorReporting.ts +++ b/frontend/src/errorReporting.ts @@ -13,12 +13,24 @@ export interface ClientErrorReport { // Fire and forget, and deliberately swallowing — the one place in this change // where swallowing is correct. This runs inside componentDidCatch, so a -// reporter that rejected would throw from the very thing that exists to stop -// throws, and there would be nothing left to catch it. +// reporter that threw — or rejected — would throw from the very thing that +// exists to stop throws, and there would be nothing left to catch it. +// +// The try/catch guards the synchronous part: JSON.stringify(report) and the +// call to fetch() itself both run before any promise exists, so React does +// not guarantee componentDidCatch is handed a real Error — code can throw +// anything, and an object with a circular message or stack makes +// JSON.stringify throw. The .catch on the returned promise guards the async +// part, the rejection once fetch has actually started. Neither covers the +// other; both are required. export function reportClientError(report: ClientErrorReport): void { - void fetch('/api/client-errors', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(report) - }).catch(() => undefined); + try { + void fetch('/api/client-errors', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(report) + }).catch(() => undefined); + } catch { + // Nothing to do — see the comment above. + } }