How React Router Handles Errors During Data Loading: A Deep Dive into Boundary-Based Error Capture
When a route's loader or action throws, React Router captures the error, creates an ErrorResponse, maps it to the nearest error boundary in the route hierarchy, and stores it in an internal errors record while clearing stale loader data via ResetLoaderDataSymbol.
React Router's data layer provides a deterministic error handling model that works uniformly across client-side navigation and server-side rendering. When failures occur during data loading, the router automatically isolates errors to specific route boundaries without crashing the entire application. Understanding how React Router handles errors during data loading is essential for building resilient, data-driven interfaces that gracefully degrade when network requests or data transformations fail.
The Seven-Stage Error Handling Pipeline
Inside packages/react-router/lib/router/router.ts, the core routing engine processes loader errors through a deterministic sequence implemented in functions like queryImpl, processRouteLoaderData, and findNearestBoundary:
- Loader execution –
queryImplruns each loader and builds aresultsmap containing either data or error objects. - Error detection –
processRouteLoaderDatainspects each result usingisErrorResultto identify thrown values or rejected promises. - Boundary resolution –
findNearestBoundary(matches, id)walks the route match stack upward to locate the closest route withhasErrorBoundary === true, falling back to the root route if none is found. - Error storage – The error is placed in the router's state under
errors[boundaryMatch.route.id], keyed by the boundary's route ID. - Data clearance – The loader's own data entry is replaced with
ResetLoaderDataSymbolto ensure UI below the boundary never receives stale data from a previous successful load. - Status extraction – If the error is an
ErrorResponse(verified viaisRouteErrorResponse(error)), its HTTP status is preserved; otherwise, the router defaults to 500. - Boundary rendering – The router renders the route's
errorElement(or<ErrorBoundary>component), which accesses the error viauseRouteError().
Boundary Resolution with findNearestBoundary
The findNearestBoundary(matches, id) function in router.ts (located near the error processing logic) implements the error bubbling mechanism. It receives the current route matches and the ID of the route that threw the error, then traverses upward through the route hierarchy until it finds a route explicitly configured with an error boundary.
If your route definition includes an errorElement or ErrorBoundary export, React Router marks that route with hasErrorBoundary === true internally. When an error originates from that route or any of its children, the boundary acts as a catchment container, preventing the error from propagating to parent routes unless explicitly allowed.
ErrorResponse Types and Status Code Handling
In packages/react-router/lib/router/utils.ts, the isRouteErrorResponse type guard distinguishes between router-generated error responses and generic JavaScript Error objects. This distinction is critical for HTTP semantics:
- Route Error Responses contain explicit
statusandstatusTextproperties that translate directly to HTTP response codes - Generic Errors default to 500 status codes when processed
During server-side rendering, this type checking allows the router to set appropriate HTTP status headers (404 for missing resources, 403 for authorization failures, etc.) rather than returning 200 OK for error pages.
Server-Side Rendering Error Flow
When an SSR request encounters a loader error, the handling logic diverges slightly to support static rendering. The getStaticContextFromError function in router.ts creates a fresh StaticHandlerContext that contains the captured error and its associated status code.
The server runtime in packages/react-router/lib/server-runtime/server.ts utilizes isRouteErrorResponse to determine whether to serialize the error for client-side hydration or treat it as a fatal server exception. This ensures that error boundaries render correctly during the initial HTML stream, allowing users to see meaningful error messages immediately while preserving the ability to hydrate interactivity on the client.
Practical Implementation: Throwing and Catching Errors
To leverage this system, developers throw Response objects from loaders and export ErrorBoundary components to render fallback UI.
// src/routes/dashboard.tsx
import { LoaderArgs } from "react-router-dom";
export async function loader({ params }: LoaderArgs) {
const user = await fetchUser(params.userId);
if (!user) {
// Throw a proper ErrorResponse so the router captures the status
throw new Response("User not found", { status: 404 });
}
return user;
}
// src/routes/dashboard.tsx
import { useRouteError, isRouteErrorResponse } from "react-router-dom";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.data?.message ?? error.statusText}</p>
</div>
);
}
return <p>Unexpected error: {error.message}</p>;
}
When the loader throws the Response object, processRouteLoaderData creates an ErrorResult, findNearestBoundary identifies the dashboard route as the boundary, and the error is stored in errors["dashboard"]. The dashboard's loaderData entry is simultaneously reset using ResetLoaderDataSymbol, ensuring that any previously cached user data doesn't leak into the error state.
Summary
- Error capture occurs in
processRouteLoaderDatawithinpackages/react-router/lib/router/router.ts, which identifies thrown values usingisErrorResult. - Boundary mapping happens via
findNearestBoundary, which walks upward through the route tree to locate the nearest parent withhasErrorBoundary === true. - State management stores errors in a dedicated
errorsrecord keyed by boundary ID while wiping stale loader data withResetLoaderDataSymbol. - Type safety relies on
isRouteErrorResponseinpackages/react-router/lib/router/utils.tsto distinguish HTTP-specific errors from generic exceptions. - SSR compatibility is provided by
getStaticContextFromError, which creates appropriate static contexts for server rendering. - HTTP semantics are preserved by extracting status codes from
ErrorResponseobjects, defaulting to 500 for unhandled exceptions.
Frequently Asked Questions
What happens if no error boundary is defined in the route hierarchy?
If findNearestBoundary reaches the root route without finding hasErrorBoundary === true, the root route becomes the error boundary by default. This ensures that errors never propagate outside the React Router tree, though it is recommended to define explicit boundaries for better user experience.
How do I throw a 404 error in a React Router loader?
Throw a Response object with status 404 from your loader function. React Router's isRouteErrorResponse will identify it as an HTTP error, preserve the 404 status code in router.state.errors, and pass it to your ErrorBoundary component where useRouteError() can access it for rendering appropriate "Not Found" UI.
Does error handling work identically in SSR and client-side rendering?
Yes, the core mechanism is identical, but SSR includes an additional serialization step. During client-side navigation, errors are rendered immediately. During SSR, getStaticContextFromError packages the error into a StaticHandlerContext so the server can stream the error boundary HTML with the correct HTTP status code before hydration.
How does React Router prevent stale data from displaying when an error occurs?
When a loader throws, processRouteLoaderData replaces that route's entry in the loaderData state with ResetLoaderDataSymbol. This symbol acts as a tombstone marker, ensuring that components receive undefined or reset data rather than cached values from previous successful loads, preventing UI inconsistencies between the error boundary and underlying route components.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →