| | |

React Router 15 ⚛️ Error Boundaries — Handling route-level errors

⏱️ Estimated read: 10 min | |

The previous chapters covered the data loaders and the form actions, which are the places where the runtime errors originate: the network failure, the missing resource, the validation failure, the unexpected exception. This chapter covers the mechanism that catches those errors and renders the fallback UI: the error boundaries. In the React Router, the route object has the errorElement property. When the loader, the action, or the component throws, the router finds the nearest errorElement in the route hierarchy and renders it instead of the broken route. The critical distinction is between the React error boundary and the React Router’s errorElement. The React error boundary is the class component that catches the render errors. The React Router’s errorElement is the route-level boundary that catches the loader, the action, and the render errors, and it has the access to the thrown error via the useRouteError hook.

Key point: The errorElement is the route property. It catches the loader errors, the action errors, and the component render errors. The useRouteError hook returns the thrown error. The error can be a Response (from the throw new Response(...)), an Error, or an unknown object. The isRouteErrorResponse helper distinguishes the Response errors from the others. The root route should always have the errorElement to catch the errors from the child routes that do not have their own.


Why error boundaries require a specific mental model

The three error sources problem. A route can fail in three ways. The loader can throw (the network failure, the missing resource). The action can throw (the validation failure, the database error). The component can throw during the render (the undefined property access, the logic bug). The errorElement catches all three. The component that does not have the errorElement crashes the entire app if the error propagates to the root. The errorElement is the isolation.

The Response versus Error problem. The throw new Response("Not Found", { status: 404 }) is the standard way to signal the HTTP error. The router catches it and passes it to the useRouteError. The throw new Error("Something went wrong") is the standard way to signal the unexpected bug. The error object is the Error instance. The isRouteErrorResponse(error) helper returns true for the Response and false for the others. The two types have different shapes and the different rendering logic.

The nearest boundary problem. The router looks for the nearest errorElement in the route hierarchy, starting from the failed route and walking up. If the /users/:id route throws, the router uses the errorElement of the /users/:id route if it has one; otherwise, it walks up to the /users route, and then to the root. The boundary is the closest ancestor that defines the errorElement. The root errorElement is the last resort.

The useRouteError problem. The hook returns the thrown error. The error is the Response for the throw new Response(...), the Error for the throw new Error(...), or an unknown object for the unexpected throws. The hook is only available inside the errorElement component. The component that uses the hook outside the errorElement receives the undefined.

The errorElement versus the ErrorBoundary component problem. The React Router provides the errorElement (the route-level) and the ErrorBoundary component (the element-level, exported by the router). The errorElement is the route configuration. The ErrorBoundary is the JSX component that can be rendered in the layout to catch the errors from a specific section. The two are complementary: the errorElement is the route, and the ErrorBoundary is the inline.

The trade-off. The errorElement is the route-level and the simplest. The ErrorBoundary is the element-level and more granular. The errorElement is the correct choice for the entire route. The ErrorBoundary is the correct choice for the subsection of the page that should not break the whole page.


a. The route-level errorElement

The errorElement is the route property. The useRouteError reads the thrown error.

import { useRouteError, isRouteErrorResponse } from "react-router";

function PostError() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{error instanceof Error ? error.message : "Unknown error"}</p>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/posts/:id",
    Component: PostDetail,
    loader: postLoader,
    errorElement: <PostError />,
  },
]);

The PostError component is the errorElement. The useRouteError returns the thrown error. The isRouteErrorResponse distinguishes the Response errors (the loader throws) from the others (the render errors). The two branches render the different messages.

The error.status and the error.statusText are the properties of the Response. The error.data is the body of the Response. The loader’s throw new Response("Not Found", { status: 404 }) sets the status and the body.


b. The root errorElement

The root route should have the errorElement as the last resort.

function RootError() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>Error {error.status}</h1>
        <p>{error.statusText}</p>
        <Link to="/">Go home</Link>
      </div>
    );
  }

  return (
    <div>
      <h1>Unexpected error</h1>
      <pre>{error instanceof Error ? error.stack : String(error)}</pre>
      <Link to="/">Go home</Link>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    errorElement: <RootError />,
    children: [
      { path: "posts", loader: postsLoader, Component: PostsList },
      { path: "users", loader: usersLoader, Component: UsersList },
    ],
  },
]);

The root errorElement catches the errors from the posts, the users, and any other child that does not have its own errorElement. The Link to="/" gives the user the recovery path.


c. The ErrorBoundary component and the nested boundaries

The React Router exports the ErrorBoundary component for the element-level boundary.

import { ErrorBoundary } from "react-router";

function App() {
  return (
    <div>
      <NavBar />
      <ErrorBoundary FallbackComponent={SectionError}>
        <Sidebar />
      </ErrorBoundary>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

function SectionError({ error }) {
  return <div className="error">Sidebar failed: {error.message}</div>;
}

The ErrorBoundary wraps the Sidebar. If the sidebar throws during the render, the SectionError renders in its place. The NavBar and the main are unaffected. The ErrorBoundary is the element-level isolation.

The useRouteError is not available inside the ErrorBoundary fallback because the ErrorBoundary is not a route. The ErrorBoundary receives the error as the prop.

The nested boundaries are the pattern.

<ErrorBoundary FallbackComponent={RootError}>
  <Header />
  <ErrorBoundary FallbackComponent={ContentError}>
    <Content />
  </ErrorBoundary>
</ErrorBoundary>

The inner boundary catches the Content errors. The outer boundary catches the errors from the inner fallback itself, which is the rare case. The nesting is the granularity.


Complete Example Session

# ============================================
# PART 1: ROUTE ERROR ELEMENT
# ============================================

const router = createBrowserRouter([
  { path: "/posts/:id", Component: Post, loader: postLoader, errorElement: <PostError /> },
]);
# ============================================
# PART 2: USE ROUTE ERROR
# ============================================

const error = useRouteError();
# ============================================
# PART 3: RESPONSE ERROR
# ============================================

if (isRouteErrorResponse(error)) {
  return <h1>{error.status}</h1>;
}
# ============================================
# PART 4: UNEXPECTED ERROR
# ============================================

return <p>{error instanceof Error ? error.message : "Unknown"}</p>;
# ============================================
# PART 5: ROOT ERROR ELEMENT
# ============================================

const router = createBrowserRouter([
  { path: "/", Component: Root, errorElement: <RootError />, children: [...] },
]);
# ============================================
# PART 6: LINK HOME
# ============================================

<Link to="/">Go home</Link>
# ============================================
# PART 7: ERROR BOUNDARY
# ============================================

<ErrorBoundary FallbackComponent={SectionError}>
  <Sidebar />
</ErrorBoundary>
# ============================================
# PART 8: SECTION ERROR
# ============================================

function SectionError({ error }) {
  return <div>Failed: {error.message}</div>;
}
# ============================================
# PART 9: VERIFY
# ============================================

# Loader throws 404 → PostError with status 404.
# Component throws → PostError with the message.
# Sidebar throws → SectionError; the rest of the page works.

The nine parts trace the error boundaries: route element, useRouteError, response error, unexpected error, root element, link home, ErrorBoundary, section error, and verification.


Quick Reference

Route Options

OptionPurpose
errorElementRoute-level error UI

Hooks

HookReturns
useRouteError()The thrown error
isRouteErrorResponse(error)Response check

Error Types

TypeSourceProperties
Responsethrow new Response(...)status, statusText, data
Errorthrow new Error(...)message, stack
UnknownOther throwsString(error)

Boundary Levels

LevelTool
RouteerrorElement
Element<ErrorBoundary>

Best Practices

✅ Do This:

if (isRouteErrorResponse(error)) {
<!-- Use the helper for the Response check -->        # ✅
{ path: "/", errorElement: <RootError />, children: [...] }
<!-- Add the root error element -->                  # ✅
<ErrorBoundary FallbackComponent={SectionError}>
<!-- Use the element-level boundary for the sections --> # ✅

❌ Don’t Do This:

# Assume the error is always an Error
<!-- It can be a Response -->                        # ❌
# Throw a plain string
<!-- It is not the Error or the Response -->         # ❌
# Forget the root error element
<!-- The unhandled error crashes the app -->         # ❌

Common Pitfalls

PitfallWhy It HappensFix
Error crashes appNo errorElementAdd the root boundary
status undefinedNot a ResponseUse isRouteErrorResponse
Section breaks pageNo ErrorBoundaryWrap the section
useRouteError undefinedUsed outside the errorElementUse inside the boundary
No recovery linkNo LinkAdd the home link

Real-World Examples

1. Route Error

errorElement: <PostError />

2. Use Route Error

const error = useRouteError();

3. Response Check

if (isRouteErrorResponse(error)) return <h1>{error.status}</h1>;

4. Error Check

if (error instanceof Error) return <p>{error.message}</p>;

5. Root Error

{ path: "/", errorElement: <RootError />, children: [...] }

6. Home Link

<Link to="/">Home</Link>

7. Error Boundary

<ErrorBoundary FallbackComponent={Fallback}>...</ErrorBoundary>

8. Fallback Component

function Fallback({ error }) { return <div>{error.message}</div>; }

9. Nested Boundaries

<ErrorBoundary><Content /></ErrorBoundary>

10. Retry

<button onClick={() => window.location.reload()}>Retry</button>

Visual

Error Sources

┌──────────────────────────────────────────────┐
│  Loader throws                               │
│  ├── Response (404)                          │
│  └── Error                                   │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Action throws                               │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Component throws                            │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Nearest errorElement                        │
└──────────────────────────────────────────────┘

Boundary Hierarchy

┌──────────────────────────────────────────────┐
│  Root errorElement                           │
│  ├── Child errorElement                      │
│  │   └── Grandchild errorElement             │
│  └── ...                                     │
└──────────────────────────────────────────────┘

Summary

ItemValue
Route errorerrorElement
Error hookuseRouteError()
Response checkisRouteErrorResponse()
Element boundary<ErrorBoundary>
Root boundaryRoot route errorElement
Recovery<Link to="/">
Error typesResponse, Error, unknown

Key takeaways:

  • The errorElement is the route-level boundary. It catches the loader errors, the action errors, and the component render errors. The nearest errorElement in the hierarchy handles the error.
  • The useRouteError returns the thrown error. The error is the Response for the throw new Response(...), the Error for the throw new Error(...), or an unknown object. The isRouteErrorResponse distinguishes the types.
  • The root errorElement is the last resort. The root route should always have the errorElement to catch the errors from the child routes that do not have their own.
  • The <ErrorBoundary> is the element-level boundary. It wraps a section of the page and catches the render errors. The rest of the page is unaffected. The errorElement is the route-level, and the ErrorBoundary is the element-level.
  • The recovery link is the UX. The Link to="/" gives the user the way out. The error message should be informative but not leak the sensitive information.

Remember: The errorElement is the route. The useRouteError is the read. The isRouteErrorResponse is the check. The <ErrorBoundary> is the element. The Link to="/" is the recovery. The exam tests the errorElement and the useRouteError.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!