Intermediate90 min read

Error Handling and Error Boundaries

Learn comprehensive error handling strategies including error boundaries, error logging, and graceful error recovery in React applications.

Topics Covered:

Error BoundariesError HandlingFallback UIError LoggingAsync ErrorsError Recovery

Prerequisites:

  • Handling Forms and User Input

Video Tutorial

Overview

Errors are inevitable in production applications. React error boundaries provide a way to catch JavaScript errors anywhere in the component tree and display fallback UI instead of crashing the entire app. This tutorial covers error boundaries (both class components and modern alternatives), error logging, handling async errors, form validation errors, error recovery patterns, and best practices for building resilient React applications. You'll learn to create graceful error experiences that inform users while providing developers with actionable error information.

Lesson 1: Understanding Errors in React

Before diving into error boundaries, it's important to understand what types of errors occur in React applications and how they behave. Types of Errors: • Render errors (component crashes) • Event handler errors (don't break UI) • Async errors (promises, setTimeout) • Lifecycle errors (useEffect, etc.) • Third-party library errors What Error Boundaries Catch: ✅ Errors during rendering ✅ Errors in lifecycle methods ✅ Errors in constructors What Error Boundaries DON'T Catch: ❌ Event handler errors ❌ Async code (setTimeout, promises) ❌ Errors during server-side rendering ❌ Errors in the error boundary itself ❌ Errors in error recovery code Default Behavior: • Without error boundaries, React unmounts the entire component tree • Users see blank screen • No way to recover • Errors logged to console only

Code Example:
// Error that WILL be caught by error boundary
function BuggyComponent() {
  const data = null;
  return <div>{data.name}</div>; // TypeError: Cannot read property 'name' of null
}

// Error that WON'T be caught (event handler)
function ComponentWithEventError() {
  const handleClick = () => {
    throw new Error('Event handler error');
  };
  return <button onClick={handleClick}>Click</button>;
}

// Error that WON'T be caught (async)
function ComponentWithAsyncError() {
  useEffect(() => {
    // This won't be caught by error boundary!
    setTimeout(() => {
      throw new Error('Async error');
    }, 1000);
  }, []);
  
  return <div>Component</div>;
}

// Error that WILL be caught (render error)
function ComponentWithRenderError() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    setData({ name: 'Test' });
  }, []);
  
  // This will be caught if data is null
  return <div>{data.name}</div>;
}

// Complete example showing error behavior
function App() {
  return (
    <div>
      {/* Without error boundary - app crashes */}
      <BuggyComponent />
      
      {/* Event handler error - doesn't crash app */}
      <ComponentWithEventError />
      
      {/* Async error - doesn't crash app but can cause issues */}
      <ComponentWithAsyncError />
    </div>
  );
}

Understanding what errors boundaries catch and don't catch is crucial. Render errors are caught, but event handler errors and async errors need different handling strategies.

Lesson 2: Introduction to Error Boundaries

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree. Key Concepts: • Error boundaries are React components • They catch errors in child components • Display fallback UI instead of crashing • Allow graceful error handling • Can log errors for monitoring Benefits: • Prevent entire app crashes • Better user experience • Error isolation • Graceful degradation • Better error visibility When to Use: • Wrap route components • Wrap feature sections • Wrap third-party components • Wrap unstable components • Wrap data-fetching components

Code Example:
// Basic error boundary concept
// Error boundaries catch errors in their child tree
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <MainContent /> {/* If this crashes, ErrorBoundary catches it */}
      <Footer />
    </ErrorBoundary>
  );
}

// Without error boundary
function AppWithoutBoundary() {
  return (
    <div>
      <Header />
      <BuggyComponent /> {/* App crashes entirely */}
      <Footer />
    </div>
  );
}

// With error boundary
function AppWithBoundary() {
  return (
    <ErrorBoundary>
      <Header />
      <BuggyComponent /> {/* Only this section shows error UI */}
      <Footer /> {/* This still renders */}
    </ErrorBoundary>
  );
}

// Multiple error boundaries for isolation
function AppWithMultipleBoundaries() {
  return (
    <ErrorBoundary fallback={<AppErrorFallback />}>
      <Header />
      
      <ErrorBoundary fallback={<FeatureErrorFallback />}>
        <MainFeature />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SidebarErrorFallback />}>
        <Sidebar />
      </ErrorBoundary>
      
      <Footer />
    </ErrorBoundary>
  );
}

Error boundaries catch errors in their child tree and display fallback UI. You can use multiple error boundaries to isolate errors and prevent cascading failures.

Lesson 3: Creating Error Boundaries with Class Components

Error boundaries must be class components. They need to implement getDerivedStateFromError or componentDidCatch (or both). Required Methods: • getDerivedStateFromError: Update state to render fallback UI • componentDidCatch: Log errors, send to monitoring service Error Boundary Lifecycle: 1. Error occurs in child component 2. getDerivedStateFromError called 3. componentDidCatch called 4. Fallback UI rendered 5. Error logged/monitored Best Practices: • Always implement both methods • Log errors to monitoring service • Provide helpful fallback UI • Allow error recovery • Reset error state when needed

Code Example:
// Basic error boundary
interface ErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
  errorInfo: React.ErrorInfo | null;
}

class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    };
  }
  
  // Called during render phase - update state to show fallback UI
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return {
      hasError: true,
      error,
    };
  }
  
  // Called after render - log error, send to monitoring
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error details
    console.error('Error caught by boundary:', error);
    console.error('Error info:', errorInfo);
    
    // Send to error monitoring service (Sentry, LogRocket, etc.)
    // logErrorToService(error, errorInfo);
    
    // Store error info for display
    this.setState({
      errorInfo,
    });
  }
  
  render() {
    if (this.state.hasError) {
      // Custom fallback UI or default
      if (this.props.fallback) {
        return this.props.fallback;
      }
      
      // Default fallback UI
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo?.componentStack}
          </details>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <MainApp />
    </ErrorBoundary>
  );
}

// Error boundary with reset capability
class ResettableErrorBoundary extends React.Component<
  { children: React.ReactNode; resetKeys?: unknown[] },
  ErrorBoundaryState
> {
  constructor(props: { children: React.ReactNode; resetKeys?: unknown[] }) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    };
  }
  
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return { hasError: true, error };
  }
  
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error:', error, errorInfo);
    this.setState({ errorInfo });
  }
  
  // Reset error state when resetKeys change
  componentDidUpdate(prevProps: { resetKeys?: unknown[] }) {
    if (
      this.state.hasError &&
      prevProps.resetKeys &&
      this.props.resetKeys &&
      prevProps.resetKeys.some((key, i) => key !== this.props.resetKeys?.[i])
    ) {
      this.setState({
        hasError: false,
        error: null,
        errorInfo: null,
      });
    }
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try again
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage with reset
function AppWithReset() {
  const [userId, setUserId] = useState(1);
  
  return (
    <ResettableErrorBoundary resetKeys={[userId]}>
      <UserProfile userId={userId} />
      <button onClick={() => setUserId(id => id + 1)}>
        Switch User
      </button>
    </ResettableErrorBoundary>
  );
}

Error boundaries are class components that implement getDerivedStateFromError (for state updates) and componentDidCatch (for error logging). You can add reset functionality by watching for prop changes.

Lesson 4: Modern Error Boundary Patterns

Since error boundaries must be class components, developers have created patterns and libraries to make them easier to use with hooks. Modern Approaches: • react-error-boundary library (recommended) • Custom hooks wrapper pattern • Higher-order component pattern • Functional component wrappers Benefits: • Simpler API • Built-in features (reset, logging) • Better TypeScript support • More flexible fallback handling • Hook-like usage patterns

Code Example:
// Using react-error-boundary library (most popular)
// npm install react-error-boundary

import { ErrorBoundary } from 'react-error-boundary';

// Simple usage
function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <MyComponent />
    </ErrorBoundary>
  );
}

// With error reset
function ErrorFallback({ error, resetErrorBoundary }: { 
  error: Error; 
  resetErrorBoundary: () => void;
}) {
  return (
    <div role="alert">
      <h2>Something went wrong:</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Reset app state, reload, etc.
        window.location.reload();
      }}
      resetKeys={['userId']} // Reset when userId changes
    >
      <MyComponent />
    </ErrorBoundary>
  );
}

// With error logging
function AppWithLogging() {
  const logError = (error: Error, errorInfo: { componentStack: string }) => {
    // Send to error monitoring service
    console.error('Logged error:', error, errorInfo);
    // sendToErrorService(error, errorInfo);
  };
  
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={logError}
    >
      <MyComponent />
    </ErrorBoundary>
  );
}

// Error boundary with retry
function ErrorFallbackWithRetry({ 
  error, 
  resetErrorBoundary 
}: { 
  error: Error; 
  resetErrorBoundary: () => void;
}) {
  const [retryCount, setRetryCount] = useState(0);
  
  const handleRetry = () => {
    setRetryCount(c => c + 1);
    resetErrorBoundary();
  };
  
  return (
    <div role="alert">
      <h2>Error occurred</h2>
      <p>{error.message}</p>
      {retryCount < 3 ? (
        <button onClick={handleRetry}>
          Retry (attempt {retryCount + 1}/3)
        </button>
      ) : (
        <p>Max retries reached. Please refresh the page.</p>
      )}
    </div>
  );
}

// Custom hook wrapper pattern
function useErrorHandler() {
  const [error, setError] = useState<Error | null>(null);
  
  if (error) {
    throw error;
  }
  
  return setError;
}

function ComponentWithErrorHandler() {
  const handleError = useErrorHandler();
  
  const handleClick = () => {
    try {
      // Something that might throw
      riskyOperation();
    } catch (error) {
      handleError(error as Error);
    }
  };
  
  return <button onClick={handleClick}>Do Risky Thing</button>;
}

function App() {
  return (
    <ErrorBoundary>
      <ComponentWithErrorHandler />
    </ErrorBoundary>
  );
}

// Higher-order component pattern
function withErrorBoundary<P extends object>(
  Component: React.ComponentType<P>,
  fallback?: React.ReactNode
) {
  return function WrappedComponent(props: P) {
    return (
      <ErrorBoundary fallback={fallback}>
        <Component {...props} />
      </ErrorBoundary>
    );
  };
}

// Usage
const SafeComponent = withErrorBoundary(UnsafeComponent, <ErrorFallback />);

function App() {
  return <SafeComponent />;
}

Modern patterns like react-error-boundary library provide simpler APIs for error boundaries. You can also create custom hooks and HOCs for reusable error handling patterns.

Lesson 5: Handling Async Errors

Error boundaries don't catch errors in async code. You need special handling for promises, setTimeout, event handlers, etc. Async Error Challenges: • Error boundaries only catch render/lifecycle errors • Async errors happen outside React's render cycle • Event handlers throw but don't trigger error boundaries • Need explicit error handling Strategies: • Try-catch in async functions • Error handling in useEffect • Promise error handlers • Error event listeners • Custom hooks for async operations

Code Example:
// ❌ ERROR: This won't be caught by error boundary
function ComponentWithAsyncError() {
  useEffect(() => {
    // Uncaught promise rejection!
    fetch('/api/data')
      .then(res => res.json())
      .then(data => {
        // If this throws, error boundary won't catch it
        console.log(data.nonexistent.property);
      });
  }, []);
  
  return <div>Component</div>;
}

// ✅ FIX: Handle async errors properly
function ComponentWithAsyncErrorHandling() {
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => {
        console.log(data);
      })
      .catch(err => {
        // Catch and set error state
        setError(err);
      });
  }, []);
  
  if (error) {
    throw error; // Now error boundary will catch it
  }
  
  return <div>Component</div>;
}

// Custom hook for async operations
function useAsync<T>(
  asyncFunction: () => Promise<T>
): {
  data: T | null;
  error: Error | null;
  loading: boolean;
} {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    asyncFunction()
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [asyncFunction]);
  
  return { data, error, loading };
}

// Usage with error boundary
function DataComponent() {
  const { data, error, loading } = useAsync(() => fetch('/api/data').then(r => r.json()));
  
  if (error) {
    throw error; // Error boundary catches this
  }
  
  if (loading) return <div>Loading...</div>;
  
  return <div>{JSON.stringify(data)}</div>;
}

// Async error boundary pattern
class AsyncErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { error: Error | null }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { error: null };
  }
  
  static getDerivedStateFromError(error: Error) {
    return { error };
  }
  
  componentDidMount() {
    // Listen for unhandled promise rejections
    window.addEventListener('unhandledrejection', this.handleRejection);
  }
  
  componentWillUnmount() {
    window.removeEventListener('unhandledrejection', this.handleRejection);
  }
  
  handleRejection = (event: PromiseRejectionEvent) => {
    this.setState({ error: event.reason });
    event.preventDefault(); // Prevent default console error
  };
  
  render() {
    if (this.state.error) {
      return <div>Error: {this.state.error.message}</div>;
    }
    return this.props.children;
  }
}

// Error handling in event handlers
function ComponentWithEventHandler() {
  const [error, setError] = useState<Error | null>(null);
  
  const handleClick = () => {
    try {
      // Risky operation
      riskyFunction();
    } catch (error) {
      setError(error as Error);
    }
  };
  
  // Throw error to trigger error boundary
  if (error) {
    throw error;
  }
  
  return <button onClick={handleClick}>Click</button>;
}

// Global error handler for uncaught errors
useEffect(() => {
  const handleError = (event: ErrorEvent) => {
    // Send to error monitoring service
    console.error('Global error:', event.error);
    // logError(event.error);
  };
  
  const handleRejection = (event: PromiseRejectionEvent) => {
    // Send to error monitoring service
    console.error('Unhandled rejection:', event.reason);
    // logError(event.reason);
  };
  
  window.addEventListener('error', handleError);
  window.addEventListener('unhandledrejection', handleRejection);
  
  return () => {
    window.removeEventListener('error', handleError);
    window.removeEventListener('unhandledrejection', handleRejection);
  };
}, []);

Async errors require explicit handling since error boundaries don't catch them. Use try-catch in async functions, handle promise rejections, and throw errors in component state to trigger error boundaries.

Lesson 6: Error Logging and Monitoring

Logging errors is crucial for debugging production issues. Learn how to integrate error monitoring services. Error Logging Benefits: • Track errors in production • Get alerts for critical errors • See error frequency and patterns • Debug with stack traces • Monitor error trends Popular Services: • Sentry (most popular) • LogRocket • Bugsnag • Rollbar • Datadog What to Log: • Error message and stack trace • Component stack • User context (userId, route) • Browser/environment info • User actions before error • Redux state (if applicable)

Code Example:
// Basic error logging utility
class ErrorLogger {
  static log(error: Error, errorInfo?: React.ErrorInfo, context?: Record<string, unknown>) {
    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error('Error:', error);
      console.error('Error Info:', errorInfo);
      console.error('Context:', context);
    }
    
    // Send to monitoring service in production
    if (process.env.NODE_ENV === 'production') {
      // Example: Send to Sentry
      // Sentry.captureException(error, {
      //   contexts: {
      //     react: { componentStack: errorInfo?.componentStack },
      //   },
      //   extra: context,
      // });
      
      // Or send to your own API
      fetch('/api/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: error.message,
          stack: error.stack,
          componentStack: errorInfo?.componentStack,
          context,
          timestamp: new Date().toISOString(),
          userAgent: navigator.userAgent,
          url: window.location.href,
        }),
      }).catch(() => {
        // Silently fail if logging fails
      });
    }
  }
}

// Error boundary with logging
class LoggingErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error with context
    ErrorLogger.log(error, errorInfo, {
      userId: this.getUserId(),
      route: window.location.pathname,
      timestamp: new Date().toISOString(),
    });
  }
  
  getUserId() {
    // Get from context, localStorage, etc.
    return localStorage.getItem('userId') || 'anonymous';
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

// Sentry integration example
// npm install @sentry/react

import * as Sentry from '@sentry/react';

// Initialize Sentry
Sentry.init({
  dsn: 'YOUR_SENTRY_DSN',
  environment: process.env.NODE_ENV,
  integrations: [
    new Sentry.BrowserTracing(),
    new Sentry.Replay(),
  ],
  tracesSampleRate: 1.0,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
});

// Wrap app with Sentry error boundary
function App() {
  return (
    <Sentry.ErrorBoundary fallback={<ErrorFallback />}>
      <MyApp />
    </Sentry.ErrorBoundary>
  );
}

// Add user context
Sentry.setUser({
  id: '123',
  email: 'user@example.com',
});

// Add breadcrumbs for context
Sentry.addBreadcrumb({
  category: 'user',
  message: 'User clicked button',
  level: 'info',
});

// Manual error reporting
try {
  riskyOperation();
} catch (error) {
  Sentry.captureException(error, {
    tags: {
      section: 'checkout',
    },
    extra: {
      cartId: cartId,
      total: total,
    },
  });
}

// Custom error boundary with Sentry
class SentryErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    Sentry.captureException(error, {
      contexts: {
        react: {
          componentStack: errorInfo.componentStack,
        },
      },
    });
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

// Error logging with retry tracking
class ErrorWithRetryLogger {
  private static retryCounts = new Map<string, number>();
  
  static log(
    error: Error,
    errorInfo?: React.ErrorInfo,
    maxRetries = 3
  ) {
    const errorKey = error.message + error.stack?.slice(0, 100);
    const retryCount = this.retryCounts.get(errorKey) || 0;
    
    if (retryCount < maxRetries) {
      this.retryCounts.set(errorKey, retryCount + 1);
      
      ErrorLogger.log(error, errorInfo, {
        retryCount: retryCount + 1,
        maxRetries,
      });
    } else {
      // Alert for repeated errors
      console.error('Error exceeded max retries:', error);
      // sendAlert(error);
    }
  }
  
  static reset(error: Error) {
    const errorKey = error.message + error.stack?.slice(0, 100);
    this.retryCounts.delete(errorKey);
  }
}

Error logging is essential for production debugging. Use error monitoring services like Sentry, or build your own logging system. Include context like user ID, route, and component stack for better debugging.

Lesson 7: Form and Validation Errors

Form errors are different from component errors. They're expected and part of normal user flow. Form Error Types: • Validation errors (client-side) • Server validation errors • Network errors • Business logic errors Best Practices: • Show errors inline with fields • Clear errors when user fixes input • Show general form errors • Prevent submission if errors exist • Use React 19 useActionState for server errors Error Display Patterns: • Inline field errors • Summary at top of form • Toast notifications • Error modals for critical errors

Code Example:
// Form error handling with React 19 useActionState
'use server';
export async function submitForm(
  prevState: { errors?: Record<string, string>; message?: string },
  formData: FormData
) {
  const errors: Record<string, string> = {};
  
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
  
  // Validation errors
  if (!email || !email.includes('@')) {
    errors.email = 'Please enter a valid email';
  }
  
  if (!password || password.length < 8) {
    errors.password = 'Password must be at least 8 characters';
  }
  
  if (Object.keys(errors).length > 0) {
    return { errors };
  }
  
  // Business logic errors
  try {
    await createUser({ email, password });
    return { message: 'Account created successfully!' };
  } catch (error) {
    if (error instanceof EmailExistsError) {
      return { errors: { email: 'This email is already registered' } };
    }
    return { errors: { _form: 'Something went wrong. Please try again.' } };
  }
}

// Client component with error handling
'use client';
import { useActionState } from 'react';

function RegistrationForm() {
  const [state, formAction, pending] = useActionState(submitForm, {});
  
  return (
    <form action={formAction}>
      {/* General form error */}
      {state.errors?._form && (
        <div className="form-error">{state.errors._form}</div>
      )}
      
      {/* Field-level errors */}
      <div>
        <input
          name="email"
          type="email"
          disabled={pending}
          aria-invalid={!!state.errors?.email}
          aria-describedby={state.errors?.email ? 'email-error' : undefined}
        />
        {state.errors?.email && (
          <span id="email-error" className="field-error" role="alert">
            {state.errors.email}
          </span>
        )}
      </div>
      
      <div>
        <input
          name="password"
          type="password"
          disabled={pending}
          aria-invalid={!!state.errors?.password}
          aria-describedby={state.errors?.password ? 'password-error' : undefined}
        />
        {state.errors?.password && (
          <span id="password-error" className="field-error" role="alert">
            {state.errors.password}
          </span>
        )}
      </div>
      
      {/* Success message */}
      {state.message && (
        <div className="success-message" role="alert">
          {state.message}
        </div>
      )}
      
      <button type="submit" disabled={pending}>
        {pending ? 'Creating Account...' : 'Register'}
      </button>
    </form>
  );
}

// Traditional form with error state
function FormWithErrorHandling() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [submitError, setSubmitError] = useState<string | null>(null);
  
  const validate = (name: string, value: string) => {
    const newErrors: Record<string, string> = {};
    
    if (name === 'email' && value && !value.includes('@')) {
      newErrors.email = 'Invalid email format';
    }
    
    if (name === 'password' && value && value.length < 8) {
      newErrors.password = 'Password must be 8+ characters';
    }
    
    setErrors(prev => ({ ...prev, ...newErrors }));
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // Clear error when user starts typing
    if (errors[name]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
    
    // Validate
    validate(name, value);
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitError(null);
    
    // Final validation
    const finalErrors: Record<string, string> = {};
    if (!formData.email.includes('@')) {
      finalErrors.email = 'Please enter a valid email';
    }
    if (formData.password.length < 8) {
      finalErrors.password = 'Password must be 8+ characters';
    }
    
    if (Object.keys(finalErrors).length > 0) {
      setErrors(finalErrors);
      return;
    }
    
    try {
      await submitForm(formData);
    } catch (error) {
      if (error instanceof NetworkError) {
        setSubmitError('Network error. Please check your connection.');
      } else if (error instanceof ValidationError) {
        setErrors(error.errors);
      } else {
        setSubmitError('An unexpected error occurred. Please try again.');
        // Log unexpected errors
        ErrorLogger.log(error as Error);
      }
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {submitError && (
        <div className="form-error" role="alert">
          {submitError}
        </div>
      )}
      
      <div>
        <input
          name="email"
          value={formData.email}
          onChange={handleChange}
          aria-invalid={!!errors.email}
        />
        {errors.email && (
          <span className="field-error" role="alert">{errors.email}</span>
        )}
      </div>
      
      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          aria-invalid={!!errors.password}
        />
        {errors.password && (
          <span className="field-error" role="alert">{errors.password}</span>
        )}
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}

// Error summary pattern
function FormWithErrorSummary() {
  const [errors, setErrors] = useState<Record<string, string>>({});
  
  const errorSummary = Object.entries(errors);
  
  return (
    <form>
      {/* Error summary at top */}
      {errorSummary.length > 0 && (
        <div className="error-summary" role="alert">
          <h3>Please fix the following errors:</h3>
          <ul>
            {errorSummary.map(([field, message]) => (
              <li key={field}>
                <a href={`#${field}`}>{message}</a>
              </li>
            ))}
          </ul>
        </div>
      )}
      
      {/* Form fields with errors */}
      {/* ... */}
    </form>
  );
}

Form errors are expected and part of normal user flow. Use React 19's useActionState for server errors, show inline field errors, clear errors as users fix issues, and provide error summaries for accessibility.

Lesson 8: Error Recovery and Resilience

Good error handling doesn't just catch errors—it helps users recover from them. Recovery Strategies: • Retry failed operations • Provide alternative actions • Save user progress • Allow graceful degradation • Show helpful error messages • Provide clear next steps Error Message Best Practices: • Use plain language • Explain what went wrong • Suggest what to do next • Avoid technical jargon • Include error codes for support • Make errors actionable

Code Example:
// Error boundary with recovery
class RecoverableErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { error: Error | null; retryCount: number }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { error: null, retryCount: 0 };
  }
  
  static getDerivedStateFromError(error: Error) {
    return { error };
  }
  
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error:', error, errorInfo);
  }
  
  handleRetry = () => {
    if (this.state.retryCount < 3) {
      this.setState(prev => ({
        error: null,
        retryCount: prev.retryCount + 1,
      }));
    } else {
      // Max retries reached
      window.location.reload();
    }
  };
  
  render() {
    if (this.state.error) {
      return (
        <div className="error-boundary">
          <h2>Oops! Something went wrong</h2>
          <p>We encountered an unexpected error. Don't worry, your data is safe.</p>
          
          {this.state.retryCount < 3 ? (
            <div>
              <p>You can try again, or refresh the page.</p>
              <button onClick={this.handleRetry}>
                Try Again ({this.state.retryCount + 1}/3)
              </button>
              <button onClick={() => window.location.reload()}>
                Refresh Page
              </button>
            </div>
          ) : (
            <div>
              <p>We're having trouble loading this page. Please refresh or contact support.</p>
              <button onClick={() => window.location.reload()}>
                Refresh Page
              </button>
              <a href="/support">Contact Support</a>
            </div>
          )}
          
          {process.env.NODE_ENV === 'development' && (
            <details>
              <summary>Error Details</summary>
              <pre>{this.state.error.stack}</pre>
            </details>
          )}
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Error with fallback content
function ComponentWithFallback() {
  const [error, setError] = useState<Error | null>(null);
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch('/api/data')
      .then(r => r.json())
      .then(setData)
      .catch(setError);
  }, []);
  
  if (error) {
    // Graceful degradation
    return (
      <div className="fallback-content">
        <h3>Unable to load content</h3>
        <p>Please try again later or contact support if the problem persists.</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }
  
  if (!data) {
    return <div>Loading...</div>;
  }
  
  return <DataDisplay data={data} />;
}

// Error with partial rendering
function ResilientComponent() {
  const [sections, setSections] = useState<{
    header: { error?: Error; data?: any };
    main: { error?: Error; data?: any };
    sidebar: { error?: Error; data?: any };
  }>({
    header: {},
    main: {},
    sidebar: {},
  });
  
  return (
    <div>
      <ErrorBoundary fallback={<HeaderFallback />}>
        <Header data={sections.header.data} />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<MainFallback />}>
        <Main data={sections.main.data} />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SidebarFallback />}>
        <Sidebar data={sections.sidebar.data} />
      </ErrorBoundary>
    </div>
  );
}

// User-friendly error messages
const ERROR_MESSAGES: Record<string, string> = {
  NETWORK_ERROR: 'Please check your internet connection and try again.',
  TIMEOUT_ERROR: 'The request took too long. Please try again.',
  NOT_FOUND: 'The requested resource could not be found.',
  UNAUTHORIZED: 'Please log in to continue.',
  FORBIDDEN: "You don't have permission to access this resource.",
  SERVER_ERROR: 'Our servers are experiencing issues. Please try again later.',
  UNKNOWN: 'Something unexpected happened. Please try again or contact support.',
};

function UserFriendlyError({ error }: { error: Error }) {
  const errorType = error.name || 'UNKNOWN';
  const message = ERROR_MESSAGES[errorType] || ERROR_MESSAGES.UNKNOWN;
  
  return (
    <div className="user-friendly-error">
      <h2>Oops!</h2>
      <p>{message}</p>
      <div className="actions">
        <button onClick={() => window.location.reload()}>Try Again</button>
        <a href="/support">Get Help</a>
      </div>
      {process.env.NODE_ENV === 'development' && (
        <details>
          <summary>Technical Details</summary>
          <pre>{error.message}</pre>
        </details>
      )}
    </div>
  );
}

// Error with auto-retry
function ComponentWithAutoRetry() {
  const [error, setError] = useState<Error | null>(null);
  const [retryCount, setRetryCount] = useState(0);
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        if (!response.ok) throw new Error('Fetch failed');
        const result = await response.json();
        setData(result);
        setError(null);
        setRetryCount(0);
      } catch (err) {
        setError(err as Error);
        
        // Auto-retry with exponential backoff
        if (retryCount < 3) {
          const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
          setTimeout(() => {
            setRetryCount(c => c + 1);
          }, delay);
        }
      }
    };
    
    fetchData();
  }, [retryCount]);
  
  if (error && retryCount >= 3) {
    return (
      <div>
        <p>Failed to load after multiple attempts.</p>
        <button onClick={() => setRetryCount(0)}>Try Again</button>
      </div>
    );
  }
  
  if (error) {
    return (
      <div>
        <p>Loading... (retry {retryCount + 1}/3)</p>
      </div>
    );
  }
  
  return <DataDisplay data={data} />;
}

Error recovery is about helping users continue after errors. Provide retry mechanisms, clear error messages, fallback content, and graceful degradation. Make errors actionable with helpful suggestions.

Lesson 9: Best Practices and Real-World Patterns

Combine all error handling strategies for production-ready applications. Best Practices Checklist: ✅ Use error boundaries strategically ✅ Log errors to monitoring service ✅ Handle async errors explicitly ✅ Provide user-friendly error messages ✅ Implement error recovery ✅ Test error scenarios ✅ Monitor error rates ✅ Handle errors at appropriate levels Strategic Error Boundary Placement: • Top level (catch-all) • Route boundaries • Feature boundaries • Third-party component boundaries • Data-fetching boundaries

Code Example:
// Production-ready error handling setup

// 1. Error boundary at app level
function App() {
  return (
    <Sentry.ErrorBoundary
      fallback={({ error, resetError }) => (
        <AppErrorFallback error={error} resetError={resetError} />
      )}
      showDialog
    >
      <ErrorBoundary fallback={<RouteErrorFallback />}>
        <Router>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/dashboard" element={
              <ErrorBoundary fallback={<DashboardErrorFallback />}>
                <Dashboard />
              </ErrorBoundary>
            } />
          </Routes>
        </Router>
      </ErrorBoundary>
    </Sentry.ErrorBoundary>
  );
}

// 2. Route-level error boundary
function RouteErrorBoundary({ children }: { children: React.ReactNode }) {
  return (
    <ErrorBoundary
      FallbackComponent={RouteErrorFallback}
      onReset={() => {
        // Navigate home on error
        window.location.href = '/';
      }}
    >
      {children}
    </ErrorBoundary>
  );
}

// 3. Feature-level error boundary
function DashboardFeature() {
  return (
    <ErrorBoundary fallback={<FeatureFallback />}>
      <ErrorBoundary fallback={<ChartFallback />}>
        <RevenueChart />
      </ErrorBoundary>
      <ErrorBoundary fallback={<TableFallback />}>
        <DataTable />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

// 4. Data-fetching error boundary
function DataFetcher({ url, children }: { url: string; children: (data: any) => React.ReactNode }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    fetch(url)
      .then(r => r.json())
      .then(setData)
      .catch(setError);
  }, [url]);
  
  if (error) {
    throw error; // Error boundary catches this
  }
  
  if (!data) {
    return <LoadingSpinner />;
  }
  
  return <>{children(data)}</>;
}

// 5. Complete error handling hook
function useErrorHandler() {
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    if (error) {
      // Log error
      ErrorLogger.log(error);
      
      // Show user notification
      // showErrorToast(error.message);
      
      // Reset after delay
      const timer = setTimeout(() => setError(null), 5000);
      return () => clearTimeout(timer);
    }
  }, [error]);
  
  const handleError = useCallback((err: Error) => {
    setError(err);
  }, []);
  
  return { error, handleError };
}

// 6. Error boundary with context
class ContextualErrorBoundary extends React.Component<
  { children: React.ReactNode; context: Record<string, unknown> },
  { error: Error | null }
> {
  constructor(props: { children: React.ReactNode; context: Record<string, unknown> }) {
    super(props);
    this.state = { error: null };
  }
  
  static getDerivedStateFromError(error: Error) {
    return { error };
  }
  
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    ErrorLogger.log(error, errorInfo, this.props.context);
  }
  
  render() {
    if (this.state.error) {
      return <ErrorFallback error={this.state.error} context={this.props.context} />;
    }
    return this.props.children;
  }
}

// 7. Testing error boundaries
function ErrorTrigger() {
  const [shouldError, setShouldError] = useState(false);
  
  if (shouldError) {
    throw new Error('Test error');
  }
  
  return (
    <button onClick={() => setShouldError(true)}>
      Trigger Error (for testing)
    </button>
  );
}

// 8. Error monitoring dashboard integration
function ErrorMonitoring() {
  useEffect(() => {
    // Track error rates
    const errorCounts = new Map<string, number>();
    
    const originalError = window.onerror;
    window.onerror = (message, source, lineno, colno, error) => {
      const key = `${source}:${lineno}`;
      errorCounts.set(key, (errorCounts.get(key) || 0) + 1);
      
      // Alert if error rate is high
      if (errorCounts.get(key)! > 10) {
        console.warn('High error rate detected:', key);
        // sendAlert({ error, count: errorCounts.get(key) });
      }
      
      if (originalError) {
        originalError(message, source, lineno, colno, error);
      }
    };
    
    return () => {
      window.onerror = originalError;
    };
  }, []);
  
  return null;
}

// Complete production setup
function ProductionApp() {
  return (
    <>
      <ErrorMonitoring />
      <Sentry.ErrorBoundary fallback={<AppErrorFallback />}>
        <ErrorBoundary fallback={<RouteErrorFallback />}>
          <App />
        </ErrorBoundary>
      </Sentry.ErrorBoundary>
    </>
  );
}

Production error handling requires multiple layers: app-level boundaries, route-level boundaries, feature boundaries, and data-fetching boundaries. Combine error boundaries with logging, monitoring, and recovery mechanisms for resilient applications.

Conclusion

Error handling is crucial for production React applications. Use error boundaries strategically throughout your component tree to catch errors, log them for debugging, and provide graceful fallback UI. Handle async errors explicitly, implement error recovery mechanisms, and always provide user-friendly error messages. Remember: error boundaries catch render/lifecycle errors, but you need explicit handling for async errors and event handlers. Combine error boundaries with monitoring services like Sentry for comprehensive error tracking and debugging.