⚛️ React Q112 / 113

How do you handle errors in a React application?

AI-Powered Answer ✓ Answered

Handling errors gracefully is crucial for building robust React applications. React provides several mechanisms to catch and manage errors, preventing application crashes and improving the user experience. This document outlines the primary strategies for error handling, including Error Boundaries, try-catch blocks, and global error listeners.

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. They are a powerful way to prevent entire applications from breaking due to a single component's error. Error Boundaries catch errors during rendering, in lifecycle methods, and constructors of the whole tree below them. However, they do not catch errors inside event handlers, asynchronous code (e.g., setTimeout or async/await), server-side rendering, or errors thrown within the Error Boundary itself.

jsx
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 the next render shows the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    console.error("Uncaught error:", error, errorInfo);
    this.setState({
      error: error,
      errorInfo: errorInfo
    });
    // Example of logging to a service (e.g., Sentry)
    // logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <div>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo && this.state.errorInfo.componentStack}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage:
// <ErrorBoundary>
//   <MyProblematicComponent />
// </ErrorBoundary>

Try-Catch Blocks for Event Handlers and Asynchronous Code

As Error Boundaries do not catch errors within event handlers or asynchronous operations (like fetch calls, setTimeout, or Promise rejections outside of rendering), it's essential to use traditional JavaScript try-catch blocks for these scenarios. This allows for specific error handling and recovery within the context of the operation.

javascript
import React, { useState } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  const handleClick = async () => {
    try {
      // Simulate an error in an event handler or async operation
      if (Math.random() > 0.5) {
        throw new Error("Failed to fetch data!");
      }
      const response = await fetch('/api/data'); // Example async call
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
      setError(null); // Clear previous errors
    } catch (err) {
      console.error("Error in handleClick:", err);
      setError(err.message);
      setData(null); // Clear data on error
      // Potentially log to a service here too
    }
  };

  return (
    <div>
      <button onClick={handleClick}>Fetch Data</button>
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

Global Error Handlers

For errors that escape both Error Boundaries and explicit try-catch blocks (e.g., unhandled promise rejections, scripts loading issues, or very early initialization errors), global error handlers can serve as a last resort. window.onerror can catch uncaught exceptions, and window.addEventListener('unhandledrejection', ...) can catch promise rejections that aren't handled by a .catch() block. These are generally used for logging purposes rather than displaying specific UI.

javascript
// In your main application entry point (e.g., index.js)
window.onerror = function (message, source, lineno, colno, error) {
  console.error("Global uncaught error:", { message, source, lineno, colno, error });
  // Send error to logging service
  // logErrorToMyService(error || new Error(message), { source, lineno, colno });
  return true; // Prevent default browser error handling
};

window.addEventListener('unhandledrejection', (event) => {
  console.error("Global unhandled promise rejection:", event.reason);
  // Send error to logging service
  // logErrorToMyService(event.reason);
  // event.preventDefault(); // Uncomment to prevent browser's default handling
});

Logging Errors to a Service

While catching errors prevents crashes, understanding why they occurred is critical for debugging and improving the application. Integrating with an error monitoring service (like Sentry, Bugsnag, Rollbar, or a custom logging backend) allows you to collect, aggregate, and analyze errors that occur in production environments. Error Boundaries' componentDidCatch method and global error handlers are prime places to send error details to such services.

Best Practices for Error Handling

  • Use Error Boundaries wisely: Wrap UI sections that are likely to fail or are critical to the user experience. Consider having one top-level Error Boundary for the whole app, and smaller, more specific ones for isolated widgets.
  • Apply try-catch for specific operations: Always use try-catch within event handlers, data fetching functions, and any other asynchronous code where an error could occur.
  • Implement global fallbacks: Use window.onerror and unhandledrejection to catch any errors that slip through, primarily for logging and understanding the scope of issues.
  • Integrate with error logging services: Send errors to a dedicated service for monitoring, alerting, and analysis in production.
  • Provide user-friendly fallback UIs: Instead of a blank screen or a broken layout, display a meaningful message or a degraded experience.
  • Test error scenarios: Actively test how your application behaves under various error conditions to ensure robust error handling.