Skip to main content

ErrorBoundary

A React component that renders when an error is thrown in a route’s loader, action, or component.

Signature

export function ErrorBoundary() {
  const error = useRouteError();
  // Render error UI
}
The ErrorBoundary is a React component with no props. It uses the useRouteError() hook to access the error that was thrown.

Basic Example

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

export async function loader() {
  const data = await fetchData();
  if (!data) {
    throw new Response("Not Found", { status: 404 });
  }
  return { data };
}

export function ErrorBoundary() {
  const error = useRouteError();

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

  return (
    <div>
      <h1>Oops!</h1>
      <p>Something went wrong.</p>
    </div>
  );
}

export default function Component() {
  // Normal rendering
}

Handling Different Error Types

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

export function ErrorBoundary() {
  const error = useRouteError();

  // Response errors (thrown via throw Response)
  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return (
        <div>
          <h1>Page Not Found</h1>
          <p>Sorry, we couldn't find what you're looking for.</p>
          <Link to="/">Go home</Link>
        </div>
      );
    }

    if (error.status === 401) {
      return (
        <div>
          <h1>Unauthorized</h1>
          <p>You don't have permission to access this.</p>
          <Link to="/login">Log in</Link>
        </div>
      );
    }

    if (error.status === 503) {
      return (
        <div>
          <h1>Service Unavailable</h1>
          <p>Looks like our API is down. Please try again later.</p>
        </div>
      );
    }

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

  // Regular JavaScript errors
  if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        {process.env.NODE_ENV === "development" && (
          <pre>{error.stack}</pre>
        )}
      </div>
    );
  }

  // Unknown error type
  return (
    <div>
      <h1>Unknown Error</h1>
      <p>An unexpected error occurred.</p>
    </div>
  );
}

Nested Error Boundaries

// app/routes/dashboard.tsx
export function ErrorBoundary() {
  return (
    <div className="dashboard-layout">
      <nav>{/* Dashboard nav still renders */}</nav>
      <main>
        <h1>Dashboard Error</h1>
        <p>Something went wrong in the dashboard.</p>
      </main>
    </div>
  );
}

export default function Dashboard() {
  return (
    <div className="dashboard-layout">
      <nav>{/* Dashboard nav */}</nav>
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
    </div>
  );
}
Error boundaries bubble up to the nearest parent route with an ErrorBoundary export:
/dashboard → Has ErrorBoundary
  /dashboard/stats → No ErrorBoundary (bubbles up)
  /dashboard/settings → Has ErrorBoundary (catches its own errors)

With Loader Data

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

export function ErrorBoundary() {
  const error = useRouteError();
  const matches = useMatches();
  
  // Get data from parent routes that loaded successfully
  const rootData = matches.find(m => m.id === "root")?.data;
  
  return (
    <div>
      {rootData?.user && (
        <header>
          <p>Logged in as {rootData.user.name}</p>
        </header>
      )}
      
      <main>
        <h1>Error</h1>
        <p>{error instanceof Error ? error.message : "Unknown error"}</p>
      </main>
    </div>
  );
}

Resetting Errors

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

export function ErrorBoundary() {
  const error = useRouteError();
  const navigate = useNavigate();

  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{error instanceof Error ? error.message : "Unknown error"}</p>
      
      <button onClick={() => navigate(0)}>
        Try again
      </button>
      
      <button onClick={() => navigate("/")}>
        Go home
      </button>
    </div>
  );
}

Throwing Errors in Components

export default function Product() {
  const { product } = useLoaderData<typeof loader>();
  
  // This will be caught by ErrorBoundary
  if (!product.isPublished) {
    throw new Response("Product not available", { status: 403 });
  }
  
  return <div>{product.name}</div>;
}

Custom Error Data

// In loader or action
export async function loader() {
  const user = await getUser();
  
  if (!user) {
    throw new Response(
      JSON.stringify({ 
        message: "Please log in",
        redirectTo: "/login" 
      }), 
      { 
        status: 401,
        headers: { "Content-Type": "application/json" }
      }
    );
  }
  
  return { user };
}

// In ErrorBoundary
export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error) && error.status === 401) {
    const data = typeof error.data === "string" 
      ? JSON.parse(error.data) 
      : error.data;
    
    return (
      <div>
        <h1>{data.message}</h1>
        <Link to={data.redirectTo}>Log in</Link>
      </div>
    );
  }
  
  return <div>An error occurred</div>;
}

Best Practices

Use Response objects for errors that are part of normal app flow:
export async function loader({ params }: Route.LoaderArgs) {
  const post = await db.post.findUnique({ 
    where: { id: params.postId } 
  });
  
  if (!post) {
    // Expected error - resource not found
    throw new Response("Post not found", { status: 404 });
  }
  
  return { post };
}
Don’t catch unexpected errors - let them reach ErrorBoundary:
// ❌ Don't do this
export async function loader() {
  try {
    return await fetchData();
  } catch (error) {
    return { error: "Something went wrong" };
  }
}

// ✅ Let errors bubble to ErrorBoundary
export async function loader() {
  return await fetchData(); // Errors automatically caught
}
Give users actionable information:
export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error) && error.status === 404) {
    return (
      <div>
        <h1>Project Not Found</h1>
        <p>
          The project you're looking for doesn't exist or has been deleted.
        </p>
        <Link to="/projects">View all projects</Link>
      </div>
    );
  }
  
  return <div>Error occurred</div>;
}
Always provide a root-level ErrorBoundary:
// app/root.tsx
export function ErrorBoundary() {
  const error = useRouteError();
  
  return (
    <html>
      <head>
        <title>Error</title>
      </head>
      <body>
        <h1>Application Error</h1>
        <p>
          {error instanceof Error 
            ? error.message 
            : "An unexpected error occurred"}
        </p>
      </body>
    </html>
  );
}

See Also

Build docs developers (and LLMs) love