Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Error Handling
Learn how to handle errors gracefully in React Router applications using error boundaries and error responses.
Overview
React Router provides a comprehensive error handling system through:
ErrorBoundary components for rendering errors
throw statements to trigger error boundaries
isRouteErrorResponse to identify different error types
- Automatic error boundary inheritance from parent routes
Basic Error Boundary
Export an ErrorBoundary component from your route:
// app/routes/profile.tsx
import { isRouteErrorResponse, useRouteError } from "react-router";
import type { Route } from "./+types/profile";
export async function loader({ params }: Route.LoaderArgs) {
const user = await getUser(params.id);
if (!user) {
throw new Response("User not found", { status: 404 });
}
return { user };
}
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>Error</h1>
<p>Something went wrong</p>
</div>
);
}
Throwing Responses
Throw HTTP responses to trigger error boundaries:
import type { Route } from "./+types/post.$id";
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPost(params.id);
if (!post) {
throw new Response("Post not found", { status: 404 });
}
if (!post.published) {
throw new Response("Post not published", { status: 403 });
}
return { post };
}
Error Responses with Data
Include structured data in error responses:
import { json } from "react-router";
import type { Route } from "./+types/api.users";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const email = formData.get("email");
const existing = await findUserByEmail(email);
if (existing) {
throw json(
{ message: "Email already in use", field: "email" },
{ status: 400 }
);
}
return { success: true };
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 400) {
return (
<div>
<h1>Validation Error</h1>
<p>{error.data.message}</p>
</div>
);
}
return <div>An error occurred</div>;
}
Handling Different Error Types
Handle various error scenarios:
import { isRouteErrorResponse, useRouteError } from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
// HTTP error responses (thrown via Response)
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return (
<div>
<h1>404 - Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>
);
}
if (error.status === 401) {
return (
<div>
<h1>401 - Unauthorized</h1>
<p>Please log in to access this page.</p>
</div>
);
}
if (error.status === 503) {
return (
<div>
<h1>503 - Service Unavailable</h1>
<p>We're experiencing technical difficulties.</p>
</div>
);
}
// Generic error response
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
// JavaScript errors (Error instances)
if (error instanceof Error) {
return (
<div>
<h1>Application Error</h1>
<p>{error.message}</p>
{process.env.NODE_ENV === "development" && (
<pre>{error.stack}</pre>
)}
</div>
);
}
// Unknown errors
return <div>Unknown error occurred</div>;
}
Global Error Boundary
Handle errors at the root level:
// app/root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
isRouteErrorResponse,
useRouteError,
} from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
let status = 500;
let message = "An unexpected error occurred.";
if (isRouteErrorResponse(error)) {
status = error.status;
message = error.data?.message || error.statusText;
} else if (error instanceof Error) {
message = error.message;
}
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{status} Error</title>
<Meta />
<Links />
</head>
<body>
<h1>{status} Error</h1>
<p>{message}</p>
<Scripts />
</body>
</html>
);
}
Error Recovery
Provide ways to recover from errors:
import { Link, useNavigate } from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
return (
<div className="error-container">
<h1>Oops!</h1>
<p>Something went wrong.</p>
<div className="error-actions">
<button onClick={() => navigate(-1)}>Go Back</button>
<Link to="/">Go Home</Link>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
{isRouteErrorResponse(error) && (
<details>
<summary>Error Details</summary>
<pre>{JSON.stringify(error.data, null, 2)}</pre>
</details>
)}
</div>
);
}
Action Errors
Handle errors from form submissions:
import { json } from "react-router";
import type { Route } from "./+types/signup";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
try {
const user = await createUser({
email: formData.get("email"),
password: formData.get("password"),
});
return redirect(`/users/${user.id}`);
} catch (error) {
if (error.code === "DUPLICATE_EMAIL") {
throw json(
{ message: "Email already exists" },
{ status: 400 }
);
}
throw error; // Re-throw unexpected errors
}
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 400) {
return (
<div>
<h2>Registration Failed</h2>
<p>{error.data.message}</p>
<Link to="/signup">Try Again</Link>
</div>
);
}
return <div>Registration error occurred</div>;
}
Nested Error Boundaries
Use route hierarchy for contextual error handling:
// app/routes/admin._index.tsx
export function ErrorBoundary() {
return (
<div className="admin-error">
<h1>Admin Panel Error</h1>
<p>Something went wrong in the admin panel.</p>
<Link to="/admin">Back to Admin</Link>
</div>
);
}
// Error bubbles to this boundary if child routes don't handle it
Error Logging
Log errors to external services:
import { useEffect } from "react";
import { useRouteError } from "react-router";
import * as Sentry from "@sentry/react";
export function ErrorBoundary() {
const error = useRouteError();
useEffect(() => {
// Log to error tracking service
if (error instanceof Error) {
Sentry.captureException(error);
} else if (isRouteErrorResponse(error)) {
Sentry.captureMessage(`HTTP ${error.status}: ${error.statusText}`);
}
}, [error]);
return <div>{/* Error UI */}</div>;
}
Development vs Production
Show detailed errors in development:
export function ErrorBoundary() {
const error = useRouteError();
const isDevelopment = process.env.NODE_ENV === "development";
return (
<div>
<h1>Error</h1>
{isDevelopment ? (
<>
{/* Detailed error info for developers */}
{error instanceof Error && (
<>
<h2>{error.message}</h2>
<pre>{error.stack}</pre>
</>
)}
{isRouteErrorResponse(error) && (
<pre>{JSON.stringify(error, null, 2)}</pre>
)}
</>
) : (
<>{/* User-friendly message for production */}
<p>We're sorry, something went wrong.</p>
</>
)}
</div>
);
}
Best Practices
- Always provide error boundaries - At least at the root level
- Use semantic status codes - 404 for not found, 403 for forbidden, etc.
- Provide recovery options - Help users get back on track
- Log errors - Track issues in production
- Show appropriate detail - More in development, less in production
- Handle expected errors - Validate and throw descriptive errors
- Use nested boundaries - Provide context-specific error handling
- Test error scenarios - Ensure error boundaries work correctly