Skip to main content

Error states

Patterns for handling failure gracefully. Every screen — 404, 500, network drop, inline field error — has a template you can drop in.

Overview

Error states are an inevitable part of any application. The goal isn't to eliminate errors entirely, but to handle them in a way that keeps users informed, maintains their confidence, and provides clear paths to resolution.

Inform

Tell users what happened in plain language

Guide

Provide actionable steps to resolve the issue

Recover

Make it easy to retry or take alternative actions

Inline Errors

Display validation errors directly next to the relevant input field. This pattern provides immediate, contextual feedback that helps users fix issues without searching for what went wrong.

Inline Error Pattern
tsx
<div>
  <label>Email address</label>
  <input className={hasError ? 'border-danger-500/60' : 'border-gray-300'} />
  {hasError && (
    <p className="mt-1.5 text-xs text-danger-400 flex items-center gap-1">
      <Cancel01Icon size={12} />
      Please enter a valid email address
    </p>
  )}
</div>

Page-Level Errors

When an entire page fails to load, replace the content area with a centered error state. Include a clear icon, descriptive message, and recovery actions like retry or navigation.

Something went wrong

We couldn't load this page. Please try again or contact support if the problem persists.

Page Error Pattern
tsx
<div className="text-center py-12">
  <div className="w-16 h-16 rounded-full bg-danger-500/10
    border border-danger-500/20 flex items-center justify-center mx-auto">
    <Alert01Icon size={28} className="text-danger-400" />
  </div>
  <h3>Something went wrong</h3>
  <p>We couldn't load this page. Please try again.</p>
  <button onClick={retry}>
    <RefreshIcon size={14} /> Try Again
  </button>
</div>

Toast Errors

Use toast notifications for non-blocking errors that don't require immediate action. Toasts appear temporarily and auto-dismiss, keeping the user's workflow uninterrupted while still surfacing the issue.

Click a button above to see toast errors

Toast Error Pattern
tsx
function showErrorToast(message: string) {
  toast.error(message, {
    duration: 4000,
    icon: <Cancel01Icon size={14} />,
    action: {
      label: 'Retry',
      onClick: () => retryLastAction(),
    },
  });
}

Empty Error States

When an API call fails and there's no cached data to show, display an illustrative empty state with a helpful message and clear recovery options. This is better than showing a blank page.

Failed to load data

The API returned an unexpected error. This might be temporary — please try again in a moment.

Network Errors

Handle offline and connectivity issues with a dedicated network error state. Include visual cues (wifi-off icon), a concise message, and an automatic or manual retry mechanism.

No connection

Check your internet connection and try again.

Network Error with Retry
tsx
function useNetworkStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

Form Validation

Combine inline field errors with a summary banner for comprehensive form validation. The banner gives an at-a-glance view of all issues, while inline messages guide users to each specific field.

Form Validation Pattern
tsx
function validateForm(values: FormValues) {
  const errors: Record<string, string> = {};

  if (!values.name.trim())
    errors.name = 'Name is required';
  if (!values.email.trim())
    errors.email = 'Email is required';
  else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email))
    errors.email = 'Invalid email format';

  return errors;
}

// Display summary banner when errors exist
{Object.keys(errors).length > 0 && (
  <div className="bg-danger-500/8 border border-danger-500/20 rounded-lg p-4">
    <p className="font-medium text-danger-400">Please fix the following:</p>
    <ul>{Object.values(errors).map(e => <li>{e}</li>)}</ul>
  </div>
)}

Error Boundaries

React Error Boundaries catch JavaScript errors in the component tree and display a fallback UI instead of crashing the entire application. Use them to isolate failures to specific sections.

ErrorBoundary.tsx
tsx
'use client';

import { Component, type ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('ErrorBoundary caught:', error, info);
    // Send to your error tracking service
    // reportError(error, info);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="text-center py-12 px-4">
          <div className="w-12 h-12 rounded-full bg-danger-500/10
            flex items-center justify-center mx-auto mb-4">
            <Alert01Icon size={20} className="text-danger-400" />
          </div>
          <h3 className="font-semibold mb-1">Something went wrong</h3>
          <p className="text-sm text-muted mb-4">
            {this.state.error?.message}
          </p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try Again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}
Usage
tsx
<ErrorBoundary fallback={<CustomFallback />}>
  <RiskyComponent />
</ErrorBoundary>

// Or wrap sections independently
<ErrorBoundary>
  <Header />
</ErrorBoundary>
<ErrorBoundary>
  <MainContent />
</ErrorBoundary>
<ErrorBoundary>
  <Sidebar />
</ErrorBoundary>

Best Practices

Follow these guidelines to create consistent, helpful error experiences across your application.

Do

  • &check;Use plain, human-readable language
  • &check;Provide specific recovery actions (retry, go back)
  • &check;Show errors close to where they occurred
  • &check;Preserve user input when displaying errors
  • &check;Log errors for debugging (console, Sentry)
  • &check;Use appropriate severity (error vs warning)
  • &check;Add animations for smooth error transitions
  • &check;Test error states during development

Don't

  • ×Show raw error codes or stack traces to users
  • ×Use generic "Something went wrong" without context
  • ×Clear form data when showing validation errors
  • ×Leave users with no way to recover
  • ×Show multiple error modals stacked on top of each other
  • ×Blame the user ("You did something wrong")
  • ×Ignore errors silently without feedback
  • ×Use red for non-error warnings or info states

Components and patterns that work well with error states.