ErrorBoundary catches route errors but doesn't log them
React Router catches errors in route modules and sends them to error boundaries to prevent blank pages. However, ErrorBoundary is not sufficient for logging and reporting errors—you need additional error handlers.
Server error reporting with handleError export
To access caught errors on the server, export a handleError function from the server entry module. This function is called whenever React Router catches an error in your application on the server. The function receives an error object and a context object containing the request. You should check if request.signal.aborted is false before logging, as React Router may abort some interrupted requests.
Server handleError function signature and example
The handleError function has type HandleErrorFunction and receives an error parameter and a context object with a request property. Example implementation:
import { type HandleErrorFunction } from "react-router";
export const handleError: HandleErrorFunction = (
error,
{ request },
) => {
if (!request.signal.aborted) {
myReportError(error);
console.error(error);
}
};
Client error reporting with onError prop
To access caught errors on the client, use the onError prop on either HydratedRouter (framework mode) or RouterProvider (data mode) component. This function is called whenever React Router catches an error in your application on the client.
Reveal client entry module with CLI
If you don't see entry.client.tsx in your app directory, you're using a default entry. Reveal it with the command: react-router reveal entry.client
Framework mode client onError function signature and example
In framework mode, the onError function has type ClientOnErrorFunction and receives an error parameter and a context object with location, params, pattern, and errorInfo properties. Example implementation:
import { type ClientOnErrorFunction } from "react-router";
const onError: ClientOnErrorFunction = (
error,
{ location, params, pattern, errorInfo },
) => {
myReportError(error, location, errorInfo);
console.error(error, errorInfo);
};
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter onError={onError} />
</StrictMode>,
);
});
Data mode client onError function signature and example
In data mode, the onError function has type ClientOnErrorFunction and receives an error parameter and a context object with location, params, pattern, and errorInfo properties. Example implementation:
import {
createBrowserRouter,
type ClientOnErrorFunction,
} from "react-router";
import { RouterProvider } from "react-router/dom";
const onError: ClientOnErrorFunction = (
error,
{ location, params, pattern, errorInfo },
) => {
myReportError(error, location, errorInfo);
console.error(error, errorInfo);
};
const router = createBrowserRouter(routes);
function App() {
return (
<RouterProvider router={router} onError={onError} />
);
}
Catch-all route should return 404 status
By default a catch-all route returns a 200 response. Modify your $.tsx catch-all route to return a 404 status. For example: export async function loader() { return data({}, 404); }
Instrumentation error handling with discriminated union result
When a handler function (loader, action, request handler, navigation, etc.) throws an error, that error will not bubble out of the callHandler function invoked from your instrumentation. Instead, the callHandler function returns a discriminated union result of type { status: 'success', error: undefined } | { status: 'error', error: Error }. This ensures your entire instrumentation function runs without needing try/catch/finally logic to handle application errors. Check the status property to determine if the handler succeeded or failed.
Instrumentation errors are gracefully swallowed
If your instrumentation function throws an error, React Router will gracefully swallow that error so it does not bubble outward and impact other instrumentations or application behavior. Errors thrown before calling the handler, after calling the handler, or within the handler will all be caught internally. All handlers and other instrumentation functions will still run.
Middleware error handling with ErrorBoundary
When middleware throws an error, it is caught and handled at the appropriate ErrorBoundary and a Response is returned through the ancestor next() call. The next() function should never throw and should always return a Response. Errors thrown after calling next() bubble up from the throwing route like normal loader errors. Errors thrown before calling next() bubble up to the highest route with a loader because no loaders have run yet and no loaderData is available.
Resource route error handling with thrown errors
Throwing an Error (or anything other than a Response or data()) from a resource route triggers handleError and results in a 500 HTTP response.
Resource route error handling with Response objects
When a resource route generates a Response (via new Response() or data()), it is considered successful execution and will not trigger handleError, even with 4xx/5xx status codes. Thrown Response objects, returned Response objects with error status codes, and data() calls with error status are all equivalent and do not trigger handleError. This aligns with fetch() behavior which does not reject on 4xx/5xx responses.
Error boundaries in resource routes
Error boundaries apply only when a resource route is accessed from a UI, such as from a fetcher call or Form submission. If you throw from a resource route in these cases, it will bubble to the nearest ErrorBoundary in the UI.
Throw data() for error status codes
To send error status codes like 404 from a loader or action, throw the `data` function result instead of returning it. For example: `throw data('Not Found', { status: 404 })` will throw to the ErrorBoundary with a 404 status code.
Register process-level unhandledRejection handler in server entry
To prevent process crashes from early promise rejections in Node, register a process-level unhandledRejection handler in entry.server.ts:
```ts
process.on("unhandledRejection", (reason, promise) => {
console.error(
"Unhandled Rejection at:",
promise,
"reason:",
reason,
);
});
```