Hardening the UI: Adding React Error Boundaries (ERR-01)
Hardening the UI: Adding React Error Boundaries (ERR-01)
Version: 0.1.90
Background
Prior to v0.1.90, the application had no root-level React error boundary. If any component in the render tree threw an unhandled error — a malformed API response, an unexpected null value in a sanctions match, a transient network blip — the entire React tree would unmount and the user would see a blank white screen with no explanation and no way to recover.
For a compliance platform where users are actively reviewing OFSI sanctions matches, this is a high-severity usability and operational risk. A frozen screen mid-review creates confusion, may force users to lose unsaved state, and erodes trust in the tool.
What Changed
Two error boundary files have been added at the root of the Next.js App Router:
src/app/error.tsx — Route-level error boundary
This file implements the Next.js error boundary contract. It is a 'use client' component that receives { error, reset } props and renders a user-facing error card.
Key behaviours:
- User-facing error card — Displays a clear, non-technical message so users know something went wrong without seeing a raw stack trace.
- Try again button — Calls
reset()to attempt re-rendering the failed segment without a full page reload, preserving as much in-session state as possible. - Scoped to the nearest route segment — errors in nested pages are caught here without taking down the entire application shell.
// src/app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>An unexpected error occurred. Your work has not been lost.</p>
<button onClick={reset}>Try again</button>
</div>
)
}
src/app/global-error.tsx — Root layout error boundary
This file catches errors thrown inside the root layout.tsx itself — a scenario that error.tsx cannot handle because it only wraps the children slot of a layout, not the layout component itself.
Key behaviours:
- Acts as the last line of defence for catastrophic render failures.
- Must render its own
<html>and<body>tags because the root layout is unavailable when this boundary activates. - Surfaces a minimal but readable error state instead of a blank screen.
// src/app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div role="alert">
<h2>A critical error occurred</h2>
<button onClick={reset}>Try again</button>
</div>
</body>
</html>
)
}
Why This Matters for Compliance Workflows
Sanctions screening is a high-stakes, time-sensitive task. Analysts reviewing an OFSI match need confidence that the tool will not silently fail. With error boundaries in place:
- No more blank screens — All unhandled render errors now produce a visible, actionable message.
- Recovery without data loss — The
reset()path attempts to re-render without a hard reload, reducing the chance of losing in-progress review state. - Operational trust — Users can distinguish between a network issue (transient, recoverable) and a genuine application fault.
Files Affected
| File | Status |
|---|---|
src/app/error.tsx | Added |
src/app/global-error.tsx | Added |
src/app/layout.tsx | Referenced (no changes required) |