Skip to main content

useRouteError

Accesses the error thrown during an action, loader, or component render to be used in a route module ErrorBoundary.
This hook only works in Data and Framework modes.

Signature

function useRouteError(): unknown

Parameters

None.

Returns

error
unknown
The error that was thrown during route loading, action execution, or rendering. Can be any value - Error objects, Response objects, or any thrown value.

Usage

Basic error boundary

import { useRouteError } from "react-router";

export function ErrorBoundary() {
  const error = useRouteError();
  
  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{error?.message || "Unknown error"}</p>
    </div>
  );
}

export async function loader() {
  throw new Error("Failed to load data");
}

export default function Component() {
  return <div>This won't render if loader throws</div>;
}

Handle Response errors

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

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }
  
  return <div>Unknown error</div>;
}

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

Different error types

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return <NotFound />;
    }
    
    if (error.status === 401) {
      return <Unauthorized />;
    }
    
    if (error.status === 503) {
      return <ServiceUnavailable />;
    }
  }
  
  if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <pre>{error.stack}</pre>
      </div>
    );
  }
  
  return <div>Unknown error occurred</div>;
}

Custom error types

class ValidationError extends Error {
  constructor(
    message: string,
    public fields: Record<string, string>
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

export async function action({ request }) {
  const formData = await request.formData();
  const email = formData.get("email");
  
  if (!email?.includes("@")) {
    throw new ValidationError("Invalid form", {
      email: "Email must be valid",
    });
  }
  
  return { success: true };
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (error instanceof ValidationError) {
    return (
      <div>
        <h2>Validation Failed</h2>
        <ul>
          {Object.entries(error.fields).map(([field, message]) => (
            <li key={field}>
              {field}: {message}
            </li>
          ))}
        </ul>
      </div>
    );
  }
  
  return <div>Error: {error?.message}</div>;
}

JSON error data

export async function loader() {
  const data = await fetchData();
  
  if (!data) {
    throw Response.json(
      { message: "Data not found", code: "NOT_FOUND" },
      { status: 404 }
    );
  }
  
  return data;
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status}</h1>
        <p>{error.data.message}</p>
        <p>Error code: {error.data.code}</p>
      </div>
    );
  }
  
  return <div>Unknown error</div>;
}

Common Patterns

404 Not Found

export async function loader({ params }) {
  const post = await db.post.findUnique({
    where: { id: params.id },
  });
  
  if (!post) {
    throw new Response("Post not found", { status: 404 });
  }
  
  return { post };
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error) && error.status === 404) {
    return (
      <div className="not-found">
        <h1>404</h1>
        <p>This post doesn't exist</p>
        <Link to="/posts">View all posts</Link>
      </div>
    );
  }
  
  throw error; // Re-throw to parent ErrorBoundary
}

Authentication errors

export async function loader({ request }) {
  const user = await requireUser(request);
  
  if (!user) {
    throw new Response("Unauthorized", { status: 401 });
  }
  
  return { user };
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error) && error.status === 401) {
    return (
      <div>
        <h1>Access Denied</h1>
        <p>You must be logged in to view this page</p>
        <Link to="/login">Go to Login</Link>
      </div>
    );
  }
  
  return <GeneralError error={error} />;
}

Development vs production errors

export function ErrorBoundary() {
  const error = useRouteError();
  const isDev = process.env.NODE_ENV === "development";
  
  if (error instanceof Error) {
    return (
      <div>
        <h1>Application Error</h1>
        <p>{error.message}</p>
        
        {isDev && (
          <details>
            <summary>Stack Trace</summary>
            <pre>{error.stack}</pre>
          </details>
        )}
        
        {!isDev && (
          <p>Please try again or contact support</p>
        )}
      </div>
    );
  }
  
  return <div>An error occurred</div>;
}

Logging errors

export function ErrorBoundary() {
  const error = useRouteError();
  
  useEffect(() => {
    // Log to error tracking service
    if (error) {
      console.error("Route error:", error);
      // Sentry.captureException(error);
    }
  }, [error]);
  
  return <div>Something went wrong</div>;
}

Retry mechanism

export function ErrorBoundary() {
  const error = useRouteError();
  const navigate = useNavigate();
  const location = useLocation();
  
  const retry = () => {
    // Force reload by navigating to same URL
    navigate(location.pathname, { replace: true });
  };
  
  return (
    <div>
      <h1>Error</h1>
      <p>{error?.message}</p>
      <button onClick={retry}>Try Again</button>
    </div>
  );
}

Nested error boundaries

// Root error boundary - catches all
export function ErrorBoundary() {
  const error = useRouteError();
  
  return (
    <html>
      <head>
        <title>Error</title>
      </head>
      <body>
        <h1>Application Error</h1>
        <p>{error?.message || "Something went wrong"}</p>
      </body>
    </html>
  );
}

// Route-specific error boundary
export function ErrorBoundary() {
  const error = useRouteError();
  
  // Handle specific errors
  if (isRouteErrorResponse(error) && error.status === 404) {
    return <RouteNotFound />;
  }
  
  // Let root boundary handle others
  throw error;
}

User-friendly error messages

const errorMessages: Record<number, string> = {
  400: "The request was invalid. Please check your input.",
  401: "You need to log in to access this page.",
  403: "You don't have permission to access this.",
  404: "We couldn't find what you're looking for.",
  500: "Our servers encountered an error. Please try again later.",
  503: "The service is temporarily unavailable.",
};

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    const message = errorMessages[error.status] || "An error occurred";
    
    return (
      <div className="error-page">
        <h1>{error.status}</h1>
        <p>{message}</p>
      </div>
    );
  }
  
  return <div>An unexpected error occurred</div>;
}

Type Safety

Type guards

function isValidationError(error: unknown): error is ValidationError {
  return error instanceof Error && error.name === "ValidationError";
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isValidationError(error)) {
    // TypeScript knows error is ValidationError
    return <div>{error.fields}</div>;
  }
  
  if (isRouteErrorResponse(error)) {
    // TypeScript knows error is Response
    return <div>{error.status}</div>;
  }
  
  if (error instanceof Error) {
    // TypeScript knows error is Error
    return <div>{error.message}</div>;
  }
  
  return <div>Unknown error</div>;
}

Custom error interface

interface AppError {
  message: string;
  code: string;
  details?: Record<string, any>;
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    const appError = error.data as AppError;
    
    return (
      <div>
        <h1>{appError.code}</h1>
        <p>{appError.message}</p>
      </div>
    );
  }
}

Important Notes

Error propagation

If an ErrorBoundary throws or re-throws an error, it propagates to the parent route’s ErrorBoundary.

Root error boundary

Always define an ErrorBoundary in your root route to catch all unhandled errors.

Render errors vs loader/action errors

The hook catches errors from:
  • Route loader functions
  • Route action functions
  • Component rendering (including child components)

Build docs developers (and LLMs) love