Why? Contains failures and enhances resilience.
Error boundaries stop a single thrown error from white-screening your entire SPA. By isolating failures at the shell and at each lazily-loaded island, you keep the rest of the interface usable, preserve user trust, and capture actionable logs for debugging.
import React, { ErrorInfo, ReactNode, Suspense, lazy } from "react";
class ErrorBoundary extends React.Component<
{ fallback: ReactNode },
{ hasError: boolean }
> {
constructor(props: { fallback: ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true }; // ① flip UI state only
}
componentDidCatch(error: unknown, info: ErrorInfo) {
console.error(error, info); // ② centralised logging
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
const ReportsPage = lazy(() => import("./features/reports/ReportsPage"));
export function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
{" "}
{/* shell boundary */}
<Header />
<main className="p-4">
<ErrorBoundary fallback={<SectionError />}>
{" "}
{/* chunk boundary */}
<Suspense fallback={<Spinner />}>
<ReportsPage />
</Suspense>
</ErrorBoundary>
</main>
</ErrorBoundary>
);
}
// ❌ Attempting to catch render errors inside a function component
function ReportsPageWrapper() {
try {
return <ReportsPage />; // errors here still bubble past the try/catch
} catch (err) {
console.error(err);
return <p>Something went wrong</p>;
}
}
// ❌ No error boundaries: any thrown error takes down the whole app
export function App() {
return (
<>
<Header />
<ReportsPageWrapper />
</>
);
}
getDerivedStateFromError
or componentDidCatch
.console.error
.setTimeout
, event callbacks)—handle those separately.