Error Boundaries

Place Error Boundaries at app shell and around separately‑loaded chunks; log and gracefully degrade UI.

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.

Correct Example

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>
  );
}

Incorrect Example

// ❌ 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 />
    </>
  );
}

Key Takeaways

  • React error boundaries must be class components (for now) and implement getDerivedStateFromError or componentDidCatch.
  • Scope boundaries strategically: one at the app shell, plus per lazy chunk or high-risk feature. Avoid wrapping every minor widget.
  • Always log the error before rendering fallback UI—Sentry/Datadog, or at least console.error.
  • Fallback UIs should be non-blocking: provide a retry button or navigation away path.
  • Error boundaries do not catch async handler failures (e.g., setTimeout, event callbacks)—handle those separately.