Hack Frontend Community

Error Boundaries in React

What are Error Boundaries?

Error Boundaries are React components that catch JavaScript errors in their child component tree, log them, and display a fallback UI instead of crashing the entire application.


Why use Error Boundaries?

Before Error Boundaries, an error in one component would break the entire app. Error Boundaries allow you to:

  • Prevent the entire application from crashing
  • Show users a friendly error message
  • Log errors for analysis
  • Isolate problematic parts of the application

Creating an Error Boundary

An Error Boundary is a class component with getDerivedStateFromError and/or componentDidCatch methods.

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state to show fallback UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Log error to reporting service
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

Usage

Wrap components that might throw errors:

function App() {
  return (
    <div>
      <ErrorBoundary>
        <MyWidget />
      </ErrorBoundary>
      
      <ErrorBoundary>
        <AnotherWidget />
      </ErrorBoundary>
    </div>
  );
}

If MyWidget throws an error, only it will crash while AnotherWidget continues working.


Difference between methods

getDerivedStateFromError

Called during rendering phase. Used to update state and show fallback UI.

static getDerivedStateFromError(error) {
  // Must return an object to update state
  return { hasError: true, error };
}

componentDidCatch

Called after rendering. Used for logging errors.

componentDidCatch(error, errorInfo) {
  // errorInfo.componentStack contains the component call stack
  logErrorToService(error, errorInfo);
}

Advanced Example

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
    });
    
    // Send to monitoring service
    logErrorToService(error, errorInfo);
  }

  handleReset = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null
    });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>An error occurred</h2>
          <details>
            <summary>Details</summary>
            <p>{this.state.error && this.state.error.toString()}</p>
            <pre>{this.state.errorInfo && this.state.errorInfo.componentStack}</pre>
          </details>
          <button onClick={this.handleReset}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

What Error Boundaries DON'T catch

Error Boundaries catch errors in:

  • Render methods
  • Lifecycle methods
  • Constructors of child components

They DON'T catch:

  • Event handlers
  • Asynchronous code (setTimeout, Promise)
  • Server-side rendering errors
  • Errors in the Error Boundary itself

Event Handlers

Use regular try-catch for event handlers:

function MyComponent() {
  function handleClick() {
    try {
      // Code that might throw an error
      somethingDangerous();
    } catch (error) {
      console.error('Error in click handler:', error);
    }
  }

  return <button onClick={handleClick}>Click me</button>;
}

Asynchronous Errors

Use try-catch for asynchronous errors as well:

function MyComponent() {
  async function fetchData() {
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      // ...
    } catch (error) {
      console.error('Fetch error:', error);
    }
  }

  useEffect(() => {
    fetchData();
  }, []);

  return <div>...</div>;
}

Where to place Error Boundaries

Global Level

function App() {
  return (
    <ErrorBoundary>
      <Router>
        <Layout>
          <Routes />
        </Layout>
      </Router>
    </ErrorBoundary>
  );
}

Route Level

function App() {
  return (
    <Router>
      <ErrorBoundary>
        <Route path="/profile" element={<Profile />} />
      </ErrorBoundary>
      
      <ErrorBoundary>
        <Route path="/settings" element={<Settings />} />
      </ErrorBoundary>
    </Router>
  );
}

Component Level

function Dashboard() {
  return (
    <div>
      <ErrorBoundary>
        <ChartWidget />
      </ErrorBoundary>
      
      <ErrorBoundary>
        <StatsWidget />
      </ErrorBoundary>
    </div>
  );
}

Integration with monitoring services

Sentry

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

class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    Sentry.captureException(error, { contexts: { react: errorInfo } });
  }

  // ...
}

// Or use Sentry's built-in ErrorBoundary
import { ErrorBoundary } from '@sentry/react';

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

React 18 and useErrorBoundary

React doesn't have a hook for Error Boundaries yet, but libraries offer solutions:

react-error-boundary

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

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

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

// Use in functional components
function MyComponent() {
  const handleError = useErrorHandler();

  async function fetchData() {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Failed to fetch');
      return response.json();
    } catch (error) {
      handleError(error);
    }
  }

  // ...
}

Best Practices

Multiple Error Boundaries

Don't use a single global Error Boundary. Use multiple at different levels:

function App() {
  return (
    <ErrorBoundary fallback={<GlobalError />}>
      <Header />
      
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<ContentError />}>
        <Content />
      </ErrorBoundary>
      
      <Footer />
    </ErrorBoundary>
  );
}

Informative Messages

Show users helpful information:

function ErrorFallback({ error }) {
  return (
    <div className="error-page">
      <h1>Oops! Something went wrong</h1>
      <p>We're working on fixing this.</p>
      <p>Try:</p>
      <ul>
        <li>Refreshing the page</li>
        <li>Going back to home</li>
        <li>Reporting the issue to us</li>
      </ul>
      {process.env.NODE_ENV === 'development' && (
        <details>
          <summary>Technical details</summary>
          <pre>{error.message}</pre>
        </details>
      )}
    </div>
  );
}

Recovery Options

Give users a way to try again:

class ErrorBoundary extends React.Component {
  state = { hasError: false, errorCount: 0 };

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

  componentDidUpdate(prevProps) {
    // Reset error when children change
    if (this.state.hasError && prevProps.children !== this.props.children) {
      this.setState({ hasError: false, errorCount: 0 });
    }
  }

  handleReset = () => {
    this.setState(state => ({
      hasError: false,
      errorCount: state.errorCount + 1
    }));
  };

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Error</h1>
          <button onClick={this.handleReset}>Try again</button>
          {this.state.errorCount > 2 && (
            <p>If the problem persists, please contact support.</p>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

Conclusion

Error Boundaries:

  • Catch errors in React components
  • Must be class components
  • Use getDerivedStateFromError and componentDidCatch
  • Don't catch errors in event handlers and async code
  • Best to use multiple at different levels
  • Allow showing fallback UI instead of blank screen
  • Essential for production applications

In interviews:

Important to be able to:

  • Explain what Error Boundaries are
  • Show how to create an Error Boundary
  • Describe which errors they don't catch
  • Explain the difference between getDerivedStateFromError and componentDidCatch
  • Give examples of proper Error Boundary placement