Skip to content

Error Boundary System

Catch React render errors and uncaught runtime errors, auto-reload on chunk-load failures, and report everything else to your error tracking service.

What Problem This Solves

You need to:

  • Catch React component crashes without crashing the entire app
  • Detect Webpack/Vite chunk-load failures and auto-reload the page
  • Report runtime errors to Slack/Sentry with environment context
  • Show a user-friendly fallback UI in production, detailed errors in dev

Prerequisites

  • react-error-boundary
  • antd (for fallback UI — replace with your UI library)

Files

error.ts

ts
const chunkFailedMessage = /Loading chunk [\d]+ failed/;
const dynamicImportFailedMessage = "dynamically imported module";

export const checkIsDynamicImportOrChunkLoadError = (error?: Error) => {
  const isErrorNameChunkLoadError = error?.name === "ChunkLoadError";
  const isErrorMessageChunkLoadFailed =
    error?.message &&
    (chunkFailedMessage.test(error.message) || error.message.includes(dynamicImportFailedMessage));

  return isErrorNameChunkLoadError || isErrorMessageChunkLoadFailed;
};

export const handleCatchBoundaryError = (
  error: Error,
  errorInfo: React.ErrorInfo,
  reporter: string = "ErrorBoundary",
) => {
  if (import.meta.env.DEV) {
    console.error("Error Info:", { error, errorInfo });
  }

  if (checkIsDynamicImportOrChunkLoadError(error)) {
    return;
  }

  const errorName = error.name ? `[${error.name}]: ` : "";
  const errorMessage = error.message ?? "";

  sendClientErrorReport({ errorName, errorMessage, reporter });
};

send-client-error-report.ts

ts
type SendClientErrorReportParams = {
  errorName: string;
  errorMessage: string;
  reporter: string;
};

export const sendClientErrorReport = async ({
  errorName,
  errorMessage,
  reporter,
}: SendClientErrorReportParams) => {
  if (import.meta.env.DEV || checkIsIgnorableError(errorMessage)) {
    return;
  }

  const errorPathWithQuery = (window?.location?.pathname ?? "") + (window?.location?.search ?? "");

  const body = {
    text: "Client-side Runtime Error",
    attachments: [
      {
        color: "#FF0000",
        fallback: "Alert",
        text: `Message: ${errorName}\`${errorMessage}\`\nPath: ${errorPathWithQuery}\nReporter: ${reporter}`,
        footer: "your-app-name",
      },
    ],
  };

  await fetch("/api/report-error", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
};

const checkIsIgnorableError = (errorMessage: string) => {
  const ignorableErrors = [
    "ResizeObserver loop completed with undelivered notifications.",
    "'text/html' is not a valid JavaScript MIME type.",
    "Importing a module script failed.",
    "Script error.",
    "TypeError: undefined is not an object",
    "Failed to execute 'removeChild' on 'Node'",
    "Invalid or unexpected token",
  ];
  return ignorableErrors.some((msg) => errorMessage.includes(msg));
};

error-boundary-wrapper.tsx

tsx
import { Button, Result, Typography } from "antd";
import React from "react";
import { type FallbackProps } from "react-error-boundary";
import { checkIsDynamicImportOrChunkLoadError } from "@/lib/utils/error";

export const ErrorBoundaryWrapper = (props: FallbackProps) => {
  const { error } = props;
  const isChunkLoadError = checkIsDynamicImportOrChunkLoadError(error);

  React.useEffect(() => {
    if (isChunkLoadError) {
      // Prevent infinite reload loops
      const hasReloaded = sessionStorage.getItem("chunk_reload");
      if (!hasReloaded) {
        sessionStorage.setItem("chunk_reload", "1");
        window.location.reload();
      }
    }
  }, [isChunkLoadError]);

  if (isChunkLoadError) {
    return null; // Will reload momentarily
  }

  return (
    <Result
      status="error"
      title="Sorry, something went wrong."
      subTitle="Our team is actively working to resolve the issue."
      extra={
        import.meta.env.DEV
          ? [
              <Button type="primary" key="retry" onClick={props.resetErrorBoundary}>
                Retry
              </Button>,
            ]
          : undefined
      }
    >
      {import.meta.env.DEV ? (
        <Typography.Paragraph>
          <pre>{error.message}</pre>
        </Typography.Paragraph>
      ) : null}
    </Result>
  );
};

use-catch-error.ts

ts
import { useEffect } from "react";
import { handleCatchBoundaryError } from "@/lib/utils/error";

export const useCatchError = () => {
  useEffect(() => {
    const handler = (event: ErrorEvent) => {
      handleCatchBoundaryError(
        event.error ?? new Error(event.message),
        { componentStack: "" } as React.ErrorInfo,
        "WindowError",
      );
    };

    window.addEventListener("error", handler);
    return () => window.removeEventListener("error", handler);
  }, []);
};

Usage

1. Wrap your app

tsx
// App.tsx
import { ErrorBoundary } from "react-error-boundary";
import { ErrorBoundaryWrapper } from "@/lib/components/error-boundary-wrapper";
import { useCatchError } from "@/lib/hooks/use-catch-error";

export function App() {
  useCatchError(); // Global window error listener

  return (
    <ErrorBoundary FallbackComponent={ErrorBoundaryWrapper}>
      <RouterProvider router={router} />
    </ErrorBoundary>
  );
}

2. Wire up error reporting endpoint

Replace the fetch('/api/report-error') call in send-client-error-report.ts with your actual error reporting service:

  • Slack: Use a webhook URL
  • Sentry: Replace with Sentry.captureException()
  • Custom: POST to your own logging endpoint

How It Works

  1. React errors are caught by react-error-boundary and rendered with ErrorBoundaryWrapper
  2. Uncaught JS errors are caught by the global window.addEventListener('error') in useCatchError
  3. Chunk-load errors trigger an immediate page reload (users get the new deployed code)
  4. All other errors are sent to your reporting endpoint, unless they match the ignorable list

Variations

With Sentry

Replace sendClientErrorReport with Sentry:

ts
import * as Sentry from "@sentry/react";

export const sendClientErrorReport = async (params: SendClientErrorReportParams) => {
  Sentry.captureMessage(`${params.errorName} ${params.errorMessage}`);
};

Without Ant Design

Replace Result and Typography in ErrorBoundaryWrapper with your own fallback UI components.