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-boundaryantd(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
- React errors are caught by
react-error-boundaryand rendered withErrorBoundaryWrapper - Uncaught JS errors are caught by the global
window.addEventListener('error')inuseCatchError - Chunk-load errors trigger an immediate page reload (users get the new deployed code)
- 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.