How would you design a global error boundary system for catching and displaying frontend errors across pages and modules
Layer error boundaries: a top-level boundary as the last resort, per-route boundaries for isolation, and granular boundaries around risky widgets. Pair with a window error/unhandledrejection handler, a logging service, user-friendly fallbacks with retry, and recovery on navigation.
A global error system has to catch errors everywhere they can occur, isolate the blast radius, report them, and let the user recover.
The layered boundary strategy
Error boundaries only catch errors in render, lifecycle, and constructors of their subtree — not events, async code, or themselves. So layer them:
- Root boundary — last line of defense. Catches anything the inner boundaries missed; shows a full-page "Something went wrong" with reload + report. Should be near-bulletproof.
- Per-route boundary — a crash in one route shows an error page for that route while the app shell survives. Resets on navigation.
- Granular boundaries — wrap independently-failing widgets (a chart, a third-party embed, a comments section) so one widget failing doesn't take down the page.
What boundaries DON'T catch — close the gaps
- Event handlers — wrap risky handlers in try/catch and route errors into a shared
reportError(or a state setter that re-throws into a boundary). - Async / promises —
window.addEventListener('unhandledrejection'). - Outside React —
window.onerror/addEventListener('error')for script errors, resource load failures. - The boundary itself — keep the root fallback dead simple so it can't throw.
Reporting & observability
- On
componentDidCatch(orreact-error-boundary'sonError), send to Sentry / Datadog / LogRocket with: error + stack, component stack, route, user/session id, app version, breadcrumbs. - Deduplicate and rate-limit so an error loop doesn't flood the service.
- Source maps uploaded so stacks are readable.
Fallback UX
- Friendly, not technical — "Something went wrong," not a stack trace.
- Recovery actions — Retry (reset the boundary), reload, go home, contact support.
- Preserve context — a widget-level fallback keeps the rest of the page usable.
- Match severity — a full-page crash vs an inline "couldn't load this section."
Recovery
react-error-boundarygivesresetErrorBoundary()andresetKeys(auto-reset when a key changes — e.g. the route or a retry counter).- Reset boundaries on route change so navigating away clears a crashed state.
Implementation
Use react-error-boundary rather than hand-writing class boundaries — it gives onError, fallbackRender, resetKeys, and a useErrorBoundary hook to push async/event errors into a boundary. React 19 also adds root-level onCaughtError/onUncaughtError options.
Follow-up questions
- •What kinds of errors do error boundaries NOT catch, and how do you handle those?
- •How do you get event-handler and async errors into a boundary?
- •What context should you attach to an error report?
- •How do you prevent an error-report flood from an error loop?
Common mistakes
- •Relying on a single root boundary — no isolation, any error blanks everything.
- •Forgetting boundaries don't catch event handlers, async code, or SSR.
- •Showing raw stack traces to users.
- •No reporting — errors crash silently in production.
- •Fallback UI that can itself throw.
Performance considerations
- •Error reporting must be rate-limited and deduplicated to avoid network floods during an error storm. Granular boundaries limit re-render blast radius. Chunk-load-error handling (force reload on stale chunks) prevents post-deploy white screens.
Edge cases
- •An error thrown inside the fallback component.
- •Error loops re-throwing on every retry.
- •Errors during SSR/hydration mismatch.
- •Chunk-load failures after a deploy (stale lazy chunks).
Real-world examples
- •Sentry integration capturing component stack + breadcrumbs on every boundary catch.
- •A dashboard where each widget is independently boundaried so one failing API doesn't kill the page.