Error Handling

Errors are in-between states we hope users never see—but they will. Network failures, database timeouts, validation errors, and unexpected exceptions are inevitable. The question isn't whether errors will occur, but whether users can recover from them gracefully.

Design components should own error presentation and recovery paths, making route files thin wrappers that compose these building blocks.

ErrorCard for Page-Level Errors

Create a reusable error display component:

export function ErrorCard({ error, reset, title = 'Something went wrong' }) { return ( <Card> <CardHeader> <AlertCircle className="text-destructive size-12" /> <CardTitle>{title}</CardTitle> </CardHeader> <CardContent> {reset && <Button onClick={reset}>Try again</Button>} </CardContent> </Card> ); } }

error.tsx Uses ErrorCard

The error.tsx route file becomes a thin wrapper:

'use client'; export default function PostError({ error, reset }) { useTrackError(error); // Log to error tracking service return ( <ErrorCard error={error} reset={reset} title="Failed to load post" description="We couldn't load this post. Please try again." /> ); }

The reset function re-renders the error boundary's children, giving the operation another chance.

ErrorBoundary for Inline Errors

For errors within a page (not full-page), use an error boundary component:

export function ErrorBoundary({ children, label }) { return ( <ReactErrorBoundary fallbackRender={({ error, resetErrorBoundary }) => ( <ErrorCard error={error} reset={resetErrorBoundary} title={label || 'Something went wrong'} /> )} > {children} </ReactErrorBoundary> ); } // Usage <ErrorBoundary label="Failed to load posts"> <PostList searchParams={searchParams} /> </ErrorBoundary>

StatusCard for Expected States

For states like 404 that aren't really "errors," use a neutral status card:

// not-found.tsx export default function NotFound() { return ( <StatusCard icon={FileQuestion} title="Post Not Found" description="This post doesn't exist or has been removed." > <BackButton href="/dashboard">Back to posts</BackButton> </StatusCard> ); }

The pattern: design components own visuals and recovery UX; route files compose them with minimal logic.

Toasts for Action Feedback

Not all errors need a full error boundary. For Server Function results—form submissions, deletions, toggles—use toasts (Sonner) to give immediate feedback without disrupting the page:

'use client'; import { useActionState } from 'react'; import { toast } from 'sonner'; const [state, formAction] = useActionState(async (prevState, formData) => { const result = await action(formData); if (result.success) { toast.success('Post saved successfully'); router.push(redirectTo); return prevState; } else { toast.error(result.error); return result.formData ?? prevState; } }, defaultValues);

The key insight: error boundaries handle unexpected failures (crashes, network errors). Toasts handle expected outcomes (validation errors, success confirmations). Use both together—error boundaries catch what you didn't anticipate, toasts communicate what you did.

October 19, 2025Updated February 24, 2026409 words