Learn

Error Handling

The recommended way to handle route errors is to place an Error Boundary inside a layout route and wrap its <Outlet />. That keeps shared UI like headers and navigation visible while errors from child routes fall back to a recovery screen.

Put the Boundary in a Layout Route

In FUNSTACK Router, child routes render inside their parent route’s <Outlet />. This makes a root layout a great place for a top-level Error Boundary: it can catch errors from all nested routes without replacing the layout itself.

A boundary outside the Router can still act as a last-resort safeguard, but it cannot use router hooks like useLocation() to reset on navigation. For day-to-day route errors, prefer a boundary inside your root layout:

import { Outlet, useLocation } from "@funstack/router";
import { ErrorBoundary } from "react-error-boundary";

function RootLayout() {
  const { entryId } = useLocation();

  return (
    <div>
      <header>My App</header>
      <nav>
        <a href="/">Home</a>
        <a href="/users/1">User</a>
      </nav>

      <ErrorBoundary
        resetKeys={[entryId]}
        fallbackRender={() => <p>Something went wrong.</p>}
      >
        <Outlet />
      </ErrorBoundary>
    </div>
  );
}

With this structure, an error in a child page, child loader, or nested layout below RootLayout shows the fallback UI while the outer layout stays mounted.

Reset the Boundary on Navigation

Error Boundaries keep showing their fallback until they are reset. The easiest way to reset them when the user navigates is to key that reset off useLocation().entryId.

entryId comes from the Navigation API’s current history entry. It changes when the router moves to a different entry, so using it in resetKeys lets the boundary recover when the user clicks another link or goes back/forward to a different page.

If you place more boundaries deeper in the tree, apply the same pattern there as well. The nearest boundary above the failing route is the one that catches the error.

How Loader Errors Propagate

Loader errors follow the same Error Boundary rules as rendering errors, but they can surface in two slightly different ways:

  • Synchronous loader throws — when React renders that route’s <Outlet />, the error is thrown there, so the nearest Error Boundary above that outlet catches it.
  • Asynchronous loader rejects — the route component receives a Promise and the rejection surfaces when you call use(data). That rejection is also caught by the nearest Error Boundary.
import { Suspense, use } from "react";

function UserPage({ data }: { data: Promise<User> }) {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserDetails data={data} />
    </Suspense>
  );
}

function UserDetails({ data }: { data: Promise<User> }) {
  const user = use(data); // A rejected loader Promise throws here
  return <h1>{user.name}</h1>;
}

This means you usually do not need special loader-specific error plumbing. Put boundaries where you want recovery UI to appear, and loader failures in that part of the route tree will bubble there.

Nested Boundaries for Finer-Grained Recovery

A root layout boundary is a good default, but you can also add boundaries to nested layouts when a section of the app should recover independently. For example, a dashboard layout can catch errors from dashboard child routes while the rest of the application keeps working.

Think of each boundary as owning the routes rendered through its <Outlet />. Put the boundary as high as needed to preserve surrounding UI, but as low as possible when you want more specific fallback screens.

See Also: How Loaders Run

This page focuses on recovery and error propagation. For when loaders execute, how results are cached, and when they re-run after navigation or form submissions, see How Loaders Run.