Error Boundaries

Handle errors gracefully with route-level error boundaries in Phyre.

Basic Error Boundary

Export an ErrorBoundary component from any route to handle errors:

jsx
// src/client/routes/posts/[id].jsx import { useRouteError } from 'react-router'; export async function loader({ params }) { const res = await fetch(`http://localhost:3000/api/posts/${params.id}`); if (!res.ok) { throw new Response('Post not found', { status: 404 }); } return res.json(); } export default function Post() { // Normal component const post = useLoaderData(); return <article>{post.title}</article>; } // Error boundary catches loader errors export function ErrorBoundary() { const error = useRouteError(); return ( <div> <h1>Error!</h1> <p>{error.statusText || error.message}</p> </div> ); }

💡 Note: Error boundaries must be named ErrorBoundary (not onError)

Using useRouteError

Access error details with the useRouteError hook:

jsx
import { useRouteError, isRouteErrorResponse } from 'react-router'; export function ErrorBoundary() { const error = useRouteError(); // Check if it's a Response error (thrown with throw new Response()) if (isRouteErrorResponse(error)) { return ( <div> <h1>{error.status} {error.statusText}</h1> <p>{error.data}</p> </div> ); } // Otherwise it's a regular Error return ( <div> <h1>Oops!</h1> <p>{error.message}</p> </div> ); }

404 Not Found

jsx
export async function loader({ params }) { const res = await fetch(`http://localhost:3000/api/posts/${params.id}`); if (res.status === 404) { throw new Response('Post not found', { status: 404 }); } return res.json(); } export function ErrorBoundary() { const error = useRouteError(); if (error.status === 404) { return ( <div className="text-center py-20"> <h1 className="text-4xl font-bold mb-4">404</h1> <p className="text-gray-400 mb-8">Post not found</p> <Link to="/posts" className="text-purple-400"> ← Back to posts </Link> </div> ); } return <div>Something went wrong</div>; }

401 Unauthorized

jsx
export async function loader({ request }) { const token = request.headers.get('Authorization'); if (!token) { throw new Response('Unauthorized', { status: 401 }); } // Fetch protected data... } export function ErrorBoundary() { const error = useRouteError(); const navigate = useNavigate(); if (error.status === 401) { return ( <div className="text-center py-20"> <h1 className="text-2xl font-bold mb-4">Access Denied</h1> <p className="text-gray-400 mb-8"> You need to be logged in to view this page </p> <button onClick={() => navigate('/login')} className="px-6 py-3 bg-purple-600 text-white rounded-lg" > Go to Login </button> </div> ); } return <div>Error: {error.message}</div>; }

500 Server Error

jsx
export async function loader() { try { const res = await fetch('http://localhost:3000/api/posts'); if (!res.ok) { throw new Response('Server error', { status: 500 }); } return res.json(); } catch (err) { throw new Response('Failed to load posts', { status: 500 }); } } export function ErrorBoundary() { const error = useRouteError(); if (error.status === 500) { return ( <div className="text-center py-20"> <h1 className="text-2xl font-bold mb-4">Server Error</h1> <p className="text-gray-400 mb-8"> Something went wrong on our end. Please try again later. </p> <button onClick={() => window.location.reload()} className="px-6 py-3 bg-purple-600 text-white rounded-lg" > Retry </button> </div> ); } return <div>Error</div>; }

Layout Error Boundaries

Error boundaries in layouts catch errors from all child routes:

jsx
// src/client/routes/_layout.jsx import { Outlet, useRouteError } from 'react-router'; export default function Layout() { return ( <div> <nav>{/* navigation */}</nav> <Outlet /> </div> ); } export function ErrorBoundary() { const error = useRouteError(); return ( <div> <nav>{/* navigation still renders */}</nav> <main className="container mx-auto px-4 py-20"> <h1 className="text-4xl font-bold mb-4">Something went wrong</h1> <p className="text-gray-400">{error.message}</p> </main> </div> ); }

Error Boundary Hierarchy

Errors bubble up to the nearest error boundary:

plaintext
routes/ ├── _layout.jsx (ErrorBoundary) ← Catches errors from all routes └── posts/ ├── _layout.jsx (ErrorBoundary) ← Catches errors from /posts/* only └── [id].jsx ← Throws error

Behavior: If [id].jsx throws an error:
1. posts/_layout.jsx ErrorBoundary catches it (if exists)
2. Otherwise, _layout.jsx ErrorBoundary catches it
3. If no ErrorBoundary exists, Phyre's default error page shows

Throwing Errors in Components

You can also throw errors from components (not just loaders):

jsx
export default function Post() { const post = useLoaderData(); if (!post.published && !post.isAuthor) { throw new Response('This post is not published', { status: 403 }); } return <article>{post.content}</article>; } export function ErrorBoundary() { const error = useRouteError(); if (error.status === 403) { return ( <div> <h1>Access Denied</h1> <p>This post is not yet published.</p> </div> ); } return <div>Error</div>; }

Custom Error Types

jsx
// Throw custom error objects export async function loader() { throw { type: 'NETWORK_ERROR', message: 'Failed to connect to server', retryable: true }; } export function ErrorBoundary() { const error = useRouteError(); if (error.type === 'NETWORK_ERROR') { return ( <div> <h1>Network Error</h1> <p>{error.message}</p> {error.retryable && ( <button onClick={() => window.location.reload()}> Retry </button> )} </div> ); } return <div>Unknown error</div>; }

Best Practices

  • Always provide error boundaries - At least one in your root layout
  • Handle specific status codes - 404, 401, 500, etc.
  • Provide helpful messages - Tell users what went wrong and what to do
  • Keep navigation visible - Don't trap users on error pages
  • Log errors - Send to error tracking service in production
  • Offer recovery actions - Retry button, back button, etc.

Production Error Logging

jsx
export function ErrorBoundary() { const error = useRouteError(); // Log to error tracking service useEffect(() => { if (process.env.NODE_ENV === 'production') { // Send to Sentry, LogRocket, etc. logError(error); } }, [error]); return <div>Error occurred</div>; }