fix(frontend): make the error reporter genuinely unable to throw (#62)

The comment claimed the reporter could not throw and the code did not deliver it. JSON.stringify(report) and the call to fetch both run synchronously, as arguments, before the promise carrying the .catch exists — so a throw from either escaped straight out of componentDidCatch, where nothing remains to catch it. The boundary that exists to stop errors would itself have been the thing that crashed.

Not merely theoretical: React does not guarantee the value handed to componentDidCatch is a real Error despite the parameter's type, because code can throw anything. An object whose message or stack is circular makes JSON.stringify throw.

The body is now wrapped in try/catch for the synchronous part, and the existing .catch still covers rejection once the request is in flight. Neither covers the other, so both are kept, and the comment now says so rather than asserting a guarantee the code did not make.

Verified: build clean, lint 0 errors and 31 warnings, unchanged.

Refs #62
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:44:50 -05:00
co-authored by Claude Opus 5
parent e0ae2dcbcd
commit b4be15ae0d
+14 -2
View File
@@ -13,12 +13,24 @@ export interface ClientErrorReport {
// Fire and forget, and deliberately swallowing — the one place in this change // Fire and forget, and deliberately swallowing — the one place in this change
// where swallowing is correct. This runs inside componentDidCatch, so a // where swallowing is correct. This runs inside componentDidCatch, so a
// reporter that rejected would throw from the very thing that exists to stop // reporter that threw — or rejected would throw from the very thing that
// throws, and there would be nothing left to catch it. // 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 { export function reportClientError(report: ClientErrorReport): void {
try {
void fetch('/api/client-errors', { void fetch('/api/client-errors', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report) body: JSON.stringify(report)
}).catch(() => undefined); }).catch(() => undefined);
} catch {
// Nothing to do — see the comment above.
}
} }