DEV Community

A0mineTV
A0mineTV

Posted on

React Error Boundaries: Building Resilient Applications That Don't Crash

When building React applications, we often focus on the happy path - making sure everything works when users interact with our app as expected. But what happens when something goes wrong? A single unhandled error can crash your entire React application, leaving users staring at a blank screen. This is where Error Boundaries come to the rescue.

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 the component tree that crashed. Think of them as a try...catch block for React components.

Error Boundaries catch errors during:

  • Rendering
  • In lifecycle methods
  • In constructors of the whole tree below them

What Error Boundaries DON'T Catch

It's important to understand the limitations:

  • Event handlers (use regular try...catch for these)
  • Asynchronous code (e.g., setTimeout or requestAnimationFrame callbacks)
  • Errors thrown during server-side rendering
  • Errors thrown in the error boundary itself

Building a Basic Error Boundary

Let's start with a simple TypeScript implementation:

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

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

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    // Update state so the next render will show the fallback UI
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log the error to your error reporting service
    console.error('Error caught by ErrorBoundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div style={{
          padding: '20px',
          border: '1px solid #ff6b6b',
          borderRadius: '8px',
          backgroundColor: '#ffe0e0',
          color: '#d63031',
          textAlign: 'center',
          margin: '20px'
        }}>
          <h2>Oops! Something went wrong</h2>
          <p>We apologize for the inconvenience. Please try refreshing the page.</p>
          <button
            onClick={() => window.location.reload()}
            style={{
              padding: '8px 16px',
              backgroundColor: '#d63031',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer'
            }}
          >
            Refresh Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;
Enter fullscreen mode Exit fullscreen mode

Advanced Error Boundary with Reset Functionality

For production applications, you'll want more sophisticated error handling. Here's an enhanced version:

import { Component } from "react";

type FallbackRenderArgs = {
  error: Error;
  reset: () => void;
}

type Props = {
  children: React.ReactNode;
  fallback?: React.ReactNode;
  fallbackRender?: (args: FallbackRenderArgs) => React.ReactNode;
  resetKeys?: unknown[];
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
  onReset?: () => void;
}

type State = {
  hasError: boolean;
  error?: Error | null;
}

export default class ErrorBoundary extends Component<Props, State> {
  state: State = {hasError: false, error: null};
  private prevResetKeys: unknown[] = [];

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

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }

  componentDidUpdate(prevProps: Props) {
    if (this.props.resetKeys && this.haveResetKeysChanged(prevProps.resetKeys || [], this.props.resetKeys || [])) {
      this.reset();
    }
  }

  private haveResetKeysChanged(prevKeys: unknown[], nextKeys: unknown[]) {
    return prevKeys.length !== nextKeys.length ||
           prevKeys.some((key, index) => key !== nextKeys[index]);
  }

  private reset = () => {
    this.setState({ hasError: false, error: null });
    this.props.onReset?.();
  }

  render() {
    if (this.state.hasError) {
      const {fallback, fallbackRender} = this.props;

      if (fallbackRender) {
        return fallbackRender({ error: this.state.error!, reset: this.reset });
      }

      if (fallback) {
        return fallback;
      }

      return (
        <div style={{
          padding: '20px',
          backgroundColor: '#fff3cd',
          border: '1px solid #ffeaa7',
          borderRadius: '8px',
          color: '#856404',
        }}>
          <h3>Something went wrong.</h3>
          <p>{this.state.error?.message}</p>
          <button onClick={this.reset}>Try again</button>
        </div>
      )
    }

    return this.props.children;
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating Custom Error Types

For better error handling, create custom error classes with additional metadata:

export class MyAppError extends Error {
  details?: {
    code?: string;
    section?: string;
    payload?: unknown;
  };

  constructor(message: string, details?: MyAppError['details']) {
    super(message);
    this.name = 'MyAppError';
    this.details = details;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Error Boundaries in Practice

Here's how to implement error boundaries in your app:

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {/* Basic usage with default fallback */}
      <ErrorBoundary>
        <SomeComponent />
      </ErrorBoundary>

      {/* With custom fallback */}
      <ErrorBoundary
        fallback={
          <div>
            <h3>Custom Error UI</h3>
            <p>Something went wrong in this section!</p>
          </div>
        }
      >
        <AnotherComponent />
      </ErrorBoundary>

      {/* With reset functionality */}
      <ErrorBoundary
        resetKeys={[count]}
        onError={(error) => console.log('Logged to monitoring service:', error)}
        onReset={() => console.log('Boundary reset')}
        fallbackRender={({ error, reset }) => (
          <div>
            <h3>Error: {error.name}</h3>
            <p>{error.message}</p>
            <button onClick={reset}>Try Again</button>
          </div>
        )}
      >
        <BuggyComponent />
      </ErrorBoundary>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Strategic Placement

Place error boundaries at strategic points in your component tree:

  • Around route components
  • Around major UI sections
  • Around third-party components

2. Granular Error Handling

Don't wrap your entire app in a single error boundary. Use multiple boundaries for different sections:

function App() {
  return (
    <div>
      <Header /> {/* Not wrapped - header errors shouldn't crash navigation */}

      <ErrorBoundary>
        <Navigation />
      </ErrorBoundary>

      <main>
        <ErrorBoundary>
          <MainContent />
        </ErrorBoundary>
      </main>

      <ErrorBoundary>
        <Sidebar />
      </ErrorBoundary>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Meaningful Error Messages

Provide helpful error messages and recovery options:

<ErrorBoundary
  fallbackRender={({ error, reset }) => (
    <div>
      <h2>Unable to load comments</h2>
      <p>There was a problem loading the comments section.</p>
      <button onClick={reset}>Retry</button>
      <button onClick={() => window.location.reload()}>
        Refresh Page
      </button>
    </div>
  )}
>
  <CommentsSection />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

4. Error Reporting

Always log errors to your monitoring service:

<ErrorBoundary
  onError={(error, errorInfo) => {
    // Log to your error monitoring service
    errorReportingService.captureException(error, {
      extra: errorInfo,
      tags: { boundary: 'comments-section' }
    });
  }}
>
  <CommentsSection />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

Testing Error Boundaries

Create a component for testing error scenarios:

function BuggyComponent({ shouldThrowError = false }: { shouldThrowError?: boolean }) {
  const [explode, setExplode] = useState(false);

  if (shouldThrowError && explode) {
    throw new Error('Boom! 💥');
  }

  return (
    <div>
      <p>I'm a potentially unstable component...</p>
      <button onClick={() => setExplode(true)}>
        Trigger Error
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

React 18 and Concurrent Features

With React 18's concurrent features, error boundaries work seamlessly with:

  • Suspense boundaries
  • Concurrent rendering
  • Automatic batching

Make sure your error boundaries are compatible with these features by avoiding side effects in render methods.

Alternatives and Complementary Patterns

1. React Query Error Handling

For data fetching errors, React Query provides excellent error handling:

function DataComponent() {
  const { data, error, isError } = useQuery('userData', fetchUser);

  if (isError) {
    return <div>Error loading user: {error.message}</div>;
  }

  return <div>{data?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

2. Error Context

For global error state management:

const ErrorContext = createContext<{
  reportError: (error: Error) => void;
}>({
  reportError: () => {}
});

function ErrorProvider({ children }: { children: ReactNode }) {
  const reportError = useCallback((error: Error) => {
    // Send to error reporting service
    console.error('Global error:', error);
  }, []);

  return (
    <ErrorContext.Provider value={{ reportError }}>
      {children}
    </ErrorContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Error boundaries are essential for building robust React applications. They provide a safety net that prevents your entire app from crashing due to unexpected errors, while giving you the opportunity to:

  • Display meaningful error messages to users
  • Log errors for debugging and monitoring
  • Provide recovery mechanisms
  • Maintain a better user experience

By implementing error boundaries strategically throughout your application, you create a more resilient user experience that gracefully handles the unexpected.

Key Takeaways

  1. Always use error boundaries in production React applications
  2. Place them strategically - not just around your entire app
  3. Provide meaningful fallback UIs with recovery options
  4. Log errors to your monitoring service for debugging
  5. Test error scenarios during development
  6. Combine with other error handling patterns for comprehensive coverage

Remember: Error boundaries are about graceful degradation, not preventing errors. Focus on providing the best possible user experience when things go wrong.

Top comments (1)

Collapse
 
anisimova_jane_11691e0963 profile image
Anisimova Jane

How wasteful some brokers can be, I sold my truck thinking I was taking a great step to greatness, not knowing I was a victim. I lost about $65k to the scammers I'm so grateful to Darek Recovery who helped me. I saw a post about the company here on blogs. I had to contact him , he asked me of proof, then he started the process, within 24 hours I got all my lost funds back. his email address is recoverydarek @ gmail com If you are n need of help they are 100% legit.