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.