Saved
Frontend Pattern

Error Boundary

Catch JavaScript errors in component trees and display fallback UI gracefully.

Difficulty Advanced

By Den Odell

Error Boundary

Problem

Uncaught JavaScript errors crash the entire application, unmounting the entire component tree and leaving users staring at a blank white screen with no explanation or way to recover. A single broken component in a sidebar widget or footer element can take down the entire page, destroying all user state and forcing a full page reload even when the primary functionality is working correctly, while users lose unsaved form data, active selections, scroll positions, and any in-progress work when errors occur.

Without error boundaries, every component error propagates up to the root, causing catastrophic failure. Errors that occur in third-party components or during data transformation crash the application with no way to contain the damage, while the browser console shows stack traces but users see nothing helpful. Development errors that slip through testing take down production applications completely, while network failures or malformed API responses that cause rendering errors crash entire pages rather than just the affected section.

Solution

Catch component errors during rendering, lifecycle methods, and child component constructors, then display fallback UI instead of crashing the entire application.

Wrap sections of the component tree in error boundary components that intercept errors thrown by descendants and render alternative UI while keeping sibling and parent components functional. This contains failures to affected sections while preserving the rest of the application.

Error boundaries can log errors to monitoring services like Sentry, Rollbar, or LogRocket for debugging production issues. They can provide reset mechanisms that allow users to retry failed operations or clear error state.

Strategic placement of error boundaries creates resilience layers - page-level boundaries prevent total crashes, section-level boundaries isolate features, and component-level boundaries contain individual widgets. Error boundaries do not catch errors in event handlers, async code, or server-side rendering, requiring separate error handling for those cases.

Example

This example demonstrates an error boundary component that catches rendering errors and displays fallback UI instead of crashing the entire application.

React Error Boundary with Logging

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

  // Called when a child component throws an error during rendering
  static getDerivedStateFromError(error) {
    // Update state to trigger fallback UI
    return { hasError: true, error };
  }

  // Called after error has been captured
  componentDidCatch(error, errorInfo) {
    // Log error to monitoring service
    console.error('Error caught by boundary:', error, errorInfo);
    
    // Send to error tracking service
    if (window.Sentry) {
      window.Sentry.captureException(error, { 
        contexts: { react: { componentStack: errorInfo.componentStack } }
      });
    }
  }

  render() {
    if (this.state.hasError) {
      // Show fallback UI if an error was caught
      return (
        <div className="error-boundary">
          <h1>Something went wrong</h1>
          <p>We've been notified and are looking into it.</p>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error?.toString()}</pre>
          </details>
        </div>
      );
    }

    // Render children normally if no error
    return this.props.children;
  }
}

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

Error Boundary with Reset

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

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

  componentDidCatch(error, errorInfo) {
    logErrorToService(error, errorInfo);
  }

  resetError = () => {
    // Clear error state to retry rendering
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Something went wrong</h1>
          <button onClick={this.resetError}>Try again</button>
        </div>
      );
    }

    return this.props.children;
  }
}

Granular Error Boundaries

function Dashboard() {
  return (
    <div>
      {/* Page-level boundary prevents total crashes */}
      <ErrorBoundary fallback={<PageError />}>
        
        {/* Section-level boundaries isolate features */}
        <ErrorBoundary fallback={<WidgetError />}>
          <SalesChart />
        </ErrorBoundary>

        <ErrorBoundary fallback={<WidgetError />}>
          <RecentActivity />
        </ErrorBoundary>

        <ErrorBoundary fallback={<WidgetError />}>
          <UserList />
        </ErrorBoundary>

      </ErrorBoundary>
    </div>
  );
}

Custom Fallback UI

function ErrorBoundary({ children, fallback, onError }) {
  const [error, setError] = useState(null);

  if (error) {
    // Render custom fallback component
    return typeof fallback === 'function' 
      ? fallback(error, () => setError(null))
      : fallback;
  }

  return children;
}

// Usage with custom fallback
<ErrorBoundary 
  fallback={(error, reset) => (
    <div>
      <h2>Widget failed to load</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
>
  <ComplexWidget />
</ErrorBoundary>

React Error Boundary Library

import { ErrorBoundary } 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() {
  const handleError = (error, errorInfo) => {
    // Log to error tracking service
    logErrorToService(error, errorInfo);
  };

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={handleError}
      onReset={() => {
        // Reset app state if needed
      }}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

Vue Error Boundary

<template>
  <div v-if="hasError" class="error-boundary">
    <h1>Something went wrong</h1>
    <p>{{ errorMessage }}</p>
    <button @click="reset">Try again</button>
  </div>
  <slot v-else></slot>
</template>

<script>
export default {
  name: 'ErrorBoundary',
  data() {
    return {
      hasError: false,
      errorMessage: ''
    };
  },
  // Catch errors from child components
  errorCaptured(err, instance, info) {
    this.hasError = true;
    this.errorMessage = err.message;

    // Log to error tracking service
    if (window.Sentry) {
      window.Sentry.captureException(err, {
        contexts: { vue: { componentName: instance.$options.name, info } }
      });
    }

    // Prevent error from propagating further
    return false;
  },
  methods: {
    reset() {
      this.hasError = false;
      this.errorMessage = '';
    }
  }
};
</script>

Vue Composition API

<script setup>
import { ref, onErrorCaptured } from 'vue';

const hasError = ref(false);
const error = ref(null);

onErrorCaptured((err, instance, info) => {
  hasError.value = true;
  error.value = err;
  
  console.error('Error captured:', err, info);
  
  // Prevent propagation
  return false;
});

function reset() {
  hasError.value = false;
  error.value = null;
}
</script>

<template>
  <div v-if="hasError">
    <h1>Error occurred</h1>
    <p>{{ error.message }}</p>
    <button @click="reset">Try again</button>
  </div>
  <slot v-else />
</template>

Svelte Error Handling

<script>
  import { onMount } from 'svelte';
  
  let hasError = false;
  let errorMessage = '';

  // Global error handler
  onMount(() => {
    const handleError = (event) => {
      if (event.error) {
        hasError = true;
        errorMessage = event.error.message;
        event.preventDefault();
      }
    };

    window.addEventListener('error', handleError);
    
    return () => {
      window.removeEventListener('error', handleError);
    };
  });

  function reset() {
    hasError = false;
    errorMessage = '';
  }
</script>

{#if hasError}
  <div class="error-boundary">
    <h1>Something went wrong</h1>
    <p>{errorMessage}</p>
    <button on:click={reset}>Try again</button>
  </div>
{:else}
  <slot />
{/if}

Web Component Error Boundary

class ErrorBoundary extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.hasError = false;
    this.errorMessage = '';
  }

  connectedCallback() {
    this.render();

    // Listen for error events from child elements
    this.addEventListener('error', (event) => {
      this.hasError = true;
      this.errorMessage = event.error?.message || 'Unknown error';
      this.render();
      event.stopPropagation();
      
      // Log to monitoring service
      console.error('Error boundary caught:', event.error);
    }, true);

    // Also handle unhandled promise rejections
    this.addEventListener('unhandledrejection', (event) => {
      this.hasError = true;
      this.errorMessage = event.reason?.message || 'Promise rejection';
      this.render();
      event.preventDefault();
    });
  }

  reset() {
    this.hasError = false;
    this.errorMessage = '';
    this.render();
  }

  render() {
    if (this.hasError) {
      this.shadowRoot.innerHTML = `
        <div class="error-boundary">
          <h1>Something went wrong</h1>
          <p>${this.errorMessage}</p>
          <button id="reset">Try again</button>
        </div>
      `;
      this.shadowRoot.getElementById('reset').addEventListener('click', () => this.reset());
    } else {
      this.shadowRoot.innerHTML = '<slot></slot>';
    }
  }
}

customElements.define('error-boundary', ErrorBoundary);

Conditional Error Boundaries

function App() {
  return (
    <div>
      {/* Only use error boundary in production */}
      {process.env.NODE_ENV === 'production' ? (
        <ErrorBoundary>
          <Dashboard />
        </ErrorBoundary>
      ) : (
        <Dashboard />
      )}
    </div>
  );
}

Benefits

  • Prevents entire application crashes from isolated component errors, keeping the application partially functional instead of showing a blank screen.
  • Provides graceful degradation with fallback UI for broken sections, showing users helpful error messages instead of cryptic browser errors.
  • Isolates failures to specific parts of the UI tree, containing damage so one broken widget doesn’t destroy the entire page.
  • Can log errors to monitoring services for debugging production issues, providing stack traces and context that help developers fix bugs.
  • Improves user experience by keeping functional parts working, allowing users to continue using unaffected features.
  • Enables progressive disclosure of errors - show simple messages to users while logging detailed technical information for developers.
  • Provides retry mechanisms that let users attempt to recover from transient failures without reloading the page.

Tradeoffs

  • Only catches errors in render, lifecycle methods, and constructors - does not catch errors in event handlers, async code like setTimeout or promises, server-side rendering, or errors thrown in the error boundary itself.
  • Requires React class components - not available as hooks, forcing functional component codebases to maintain class components specifically for error boundaries or use third-party libraries.
  • Can hide bugs if fallback UI is too generic or unclear - users see “something went wrong” without understanding what broke or how to proceed, and developers may not investigate if errors seem handled.
  • May mask underlying issues that should be fixed - error boundaries treat symptoms rather than causes, and poorly placed boundaries might catch errors that should propagate for proper handling.
  • Requires strategic placement to balance granularity and coverage - too few boundaries create coarse error handling that takes down large sections, while too many create complex error UI and recovery logic.
  • Event handler errors require separate try-catch blocks - developers must remember to wrap event handlers manually since error boundaries don’t catch those errors automatically.
  • Async errors in useEffect or data fetching require explicit error state management - error boundaries don’t catch promise rejections or errors in async functions.
  • Error boundaries catch errors during render but the error state itself can cause new errors if fallback UI has bugs, potentially creating error loops that are difficult to debug.
  • Reset functionality requires careful consideration of what state to reset - simply clearing error state may cause the same error to occur again if underlying conditions haven’t changed.
  • Multiple nested error boundaries create complexity in determining which boundary catches which errors and what fallback UI displays where.
  • Testing error boundaries requires intentionally throwing errors in tests, which can be awkward and may require disabling console.error to avoid test output pollution.
  • Error boundary placement decisions are difficult - should each component have its own boundary, or should boundaries wrap larger sections? The right answer depends on the application structure and failure modes.
  • Production error boundaries may catch errors that would help developers debug in development, requiring conditional boundaries or error rethrowing in development environments.
Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.