🛡️ Error Boundaries

Graceful Error Handling in React

What are Error Boundaries?

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire app.

🎯 Basic Error Boundary

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }
  
  static getDerivedStateFromError(error) {
    // Update state so next render shows fallback UI
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log error to error reporting service
    console.error('Error caught by boundary:', error, errorInfo);
    
    this.setState({
      error: error,
      errorInfo: errorInfo
    });
    
    // Send to monitoring service (e.g., Sentry)
    // logErrorToService(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>Something went wrong</h2>
          <p>We're sorry for the inconvenience.</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <MainContent />
      <Footer />
    </ErrorBoundary>
  );
}

🔄 Error Boundary with Reset

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error(error, errorInfo);
  }
  
  resetError = () => {
    this.setState({ hasError: false });
  };
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Oops! Something went wrong</h2>
          <button onClick={this.resetError}>Try Again</button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage with key to force remount
function App() {
  const [resetKey, setResetKey] = useState(0);
  
  return (
    <ErrorBoundary key={resetKey}>
      <ProblematicComponent />
      <button onClick={() => setResetKey(k => k + 1)}>
        Reset
      </button>
    </ErrorBoundary>
  );
}

📝 Detailed Error Display (Development)

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    this.setState({ error, errorInfo });
  }
  
  render() {
    if (this.state.hasError) {
      const isDevelopment = process.env.NODE_ENV === 'development';
      
      return (
        <div style={{ padding: '20px', border: '2px solid red' }}>
          <h2>💥 Error Occurred</h2>
          
          {isDevelopment && (
            <>
              <details style={{ whiteSpace: 'pre-wrap' }}>
                <summary>Error Details</summary>
                <p><strong>Error:</strong> {this.state.error?.toString()}</p>
                <p><strong>Stack Trace:</strong></p>
                <pre>{this.state.errorInfo?.componentStack}</pre>
              </details>
            </>
          )}
          
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

🎨 Custom Fallback Component

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      // Use custom fallback if provided
      return this.props.fallback || <DefaultErrorUI />;
    }
    
    return this.props.children;
  }
}

// Custom fallback components
function MinimalErrorUI() {
  return <div>⚠️ Error occurred</div>;
}

function FriendlyErrorUI() {
  return (
    <div className="friendly-error">
      <h2>😔 Oops!</h2>
      <p>Something unexpected happened.</p>
      <button onClick={() => window.location.href = '/'}>
        Go Home
      </button>
    </div>
  );
}

// Usage with custom fallback
<ErrorBoundary fallback={<FriendlyErrorUI />}>
  <MyComponent />
</ErrorBoundary>

🎯 Multiple Error Boundaries

function App() {
  return (
    <div>
      {/* Global error boundary */}
      <ErrorBoundary fallback={<AppErrorFallback />}>
        
        <Header />
        
        {/* Sidebar error boundary */}
        <ErrorBoundary fallback={<SidebarError />}>
          <Sidebar />
        </ErrorBoundary>
        
        {/* Main content error boundary */}
        <ErrorBoundary fallback={<ContentError />}>
          <MainContent />
        </ErrorBoundary>
        
        <Footer />
        
      </ErrorBoundary>
    </div>
  );
}

// Benefits:
// 1. Isolated failures - sidebar error won't crash main content
// 2. Specific fallback UI for each section
// 3. Better user experience - rest of app still works

📊 Error Logging Service Integration

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

// Initialize Sentry
Sentry.init({
  dsn: 'your-sentry-dsn',
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
});

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, eventId: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log to Sentry
    Sentry.withScope((scope) => {
      scope.setExtras(errorInfo);
      const eventId = Sentry.captureException(error);
      this.setState({ eventId });
    });
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Something went wrong</h2>
          <button
            onClick={() => {
              Sentry.showReportDialog({ 
                eventId: this.state.eventId 
              });
            }}
          >
            Report Feedback
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Or use Sentry's built-in error boundary
const SentryErrorBoundary = Sentry.ErrorBoundary;

function App() {
  return (
    <SentryErrorBoundary fallback={<ErrorFallback />}>
      <MyApp />
    </SentryErrorBoundary>
  );
}

⚠️ What Error Boundaries DON'T Catch

// ❌ Error boundaries DO NOT catch:

// 1. Event handlers (use try-catch)
function MyComponent() {
  const handleClick = () => {
    try {
      throw new Error('Event handler error');
    } catch (error) {
      console.error(error);
    }
  };
  
  return <button onClick={handleClick}>Click</button>;
}

// 2. Asynchronous code (use try-catch)
function MyComponent() {
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        // ...
      } catch (error) {
        console.error(error);
      }
    };
    
    fetchData();
  }, []);
}

// 3. Server-side rendering

// 4. Errors in error boundary itself

// ✅ Error boundaries DO catch:
// - Rendering errors
// - Lifecycle method errors
// - Constructor errors in child components

🔧 react-error-boundary Library

// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';

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

// Usage
function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Reset app state
      }}
      onError={(error, errorInfo) => {
        // Log error
        console.error(error, errorInfo);
      }}
    >
      <MyApp />
    </ErrorBoundary>
  );
}

// With reset keys
function User({ userId }) {
  const { data: user } = useQuery(['user', userId], fetchUser);
  
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      resetKeys={[userId]}  // Reset when userId changes
    >
      <UserProfile user={user} />
    </ErrorBoundary>
  );
}

// useErrorHandler hook
import { useErrorHandler } from 'react-error-boundary';

function MyComponent() {
  const handleError = useErrorHandler();
  
  const fetchData = async () => {
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
    } catch (error) {
      handleError(error);  // Trigger error boundary
    }
  };
  
  return <button onClick={fetchData}>Fetch</button>;
}

🎯 Best Practices

📋 Complete Example

import { ErrorBoundary } from 'react-error-boundary';
import * as Sentry from '@sentry/react';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div className="error-container">
      <h2>Application Error</h2>
      <p>We're experiencing technical difficulties.</p>
      <details>
        <summary>Error details</summary>
        <pre>{error.message}</pre>
      </details>
      <button onClick={resetErrorBoundary}>Try Again</button>
      <button onClick={() => window.location.href = '/'}>Go Home</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, errorInfo) => {
        // Log to Sentry
        Sentry.captureException(error, { extra: errorInfo });
      }}
      onReset={() => {
        // Reset app state
        window.location.reload();
      }}
    >
      <Router>
        <Header />
        
        <ErrorBoundary fallback={<SidebarError />}>
          <Sidebar />
        </ErrorBoundary>
        
        <ErrorBoundary fallback={<ContentError />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/dashboard" element={<Dashboard />} />
          </Routes>
        </ErrorBoundary>
        
        <Footer />
      </Router>
    </ErrorBoundary>
  );
}

🎯 Key Takeaways