Skip to main content

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

  1. Always provide error boundaries - At least at the root level
  2. Use semantic status codes - 404 for not found, 403 for forbidden, etc.
  3. Provide recovery options - Help users get back on track
  4. Log errors - Track issues in production
  5. Show appropriate detail - More in development, less in production
  6. Handle expected errors - Validate and throw descriptive errors
  7. Use nested boundaries - Provide context-specific error handling
  8. Test error scenarios - Ensure error boundaries work correctly

Build docs developers (and LLMs) love