# How React Router Handles Errors During Data Loading: A Deep Dive into Boundary-Based Error Capture

> Discover how React Router captures and handles data loading errors. Learn about error boundaries and the error response process for seamless application error management.

- Repository: [Remix/react-router](https://github.com/remix-run/react-router)
- Tags: deep-dive
- Published: 2026-03-06

---

**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`](https://github.com/remix-run/react-router/blob/main/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`:

1. **Loader execution** – `queryImpl` runs each loader and builds a `results` map containing either data or error objects.
2. **Error detection** – `processRouteLoaderData` inspects each result using `isErrorResult` to identify thrown values or rejected promises.
3. **Boundary resolution** – `findNearestBoundary(matches, id)` walks the route match stack upward to locate the closest route with `hasErrorBoundary === true`, falling back to the root route if none is found.
4. **Error storage** – The error is placed in the router's state under `errors[boundaryMatch.route.id]`, keyed by the boundary's route ID.
5. **Data clearance** – The loader's own data entry is replaced with `ResetLoaderDataSymbol` to ensure UI below the boundary never receives stale data from a previous successful load.
6. **Status extraction** – If the error is an `ErrorResponse` (verified via `isRouteErrorResponse(error)`), its HTTP status is preserved; otherwise, the router defaults to 500.
7. **Boundary rendering** – The router renders the route's `errorElement` (or `<ErrorBoundary>` component), which accesses the error via `useRouteError()`.

## Boundary Resolution with findNearestBoundary

The `findNearestBoundary(matches, id)` function in [`router.ts`](https://github.com/remix-run/react-router/blob/main/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`](https://github.com/remix-run/react-router/blob/main/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 `status` and `statusText` properties 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`](https://github.com/remix-run/react-router/blob/main/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`](https://github.com/remix-run/react-router/blob/main/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.

```tsx
// 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;
}

```

```tsx
// 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 `processRouteLoaderData` within [`packages/react-router/lib/router/router.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/router.ts), which identifies thrown values using `isErrorResult`.
- **Boundary mapping** happens via `findNearestBoundary`, which walks upward through the route tree to locate the nearest parent with `hasErrorBoundary === true`.
- **State management** stores errors in a dedicated `errors` record keyed by boundary ID while wiping stale loader data with `ResetLoaderDataSymbol`.
- **Type safety** relies on `isRouteErrorResponse` in [`packages/react-router/lib/router/utils.ts`](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts) to 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 `ErrorResponse` objects, 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.