⚛️ React Q107 / 113

How do you handle errors in a React application?

AI-Powered Answer ✓ Answered

Handling errors gracefully is crucial for building robust and user-friendly React applications. React provides several mechanisms to manage errors that occur during rendering, in lifecycle methods, and within asynchronous operations or event handlers.

1. 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 crashed component tree. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

jsx
import React, { Component } from 'react';

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

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

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    console.error("ErrorBoundary caught an error:", error, errorInfo);
    this.setState({ error: error, errorInfo: 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.componentStack}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

To use an Error Boundary, you wrap a part of your component tree with it. If an error occurs within the wrapped components, the Error Boundary will catch it.

jsx
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyProblematicComponent from './MyProblematicComponent'; // A component that might throw errors

function App() {
  return (
    <div>
      <h1>Application Header</h1>
      <ErrorBoundary>
        <MyProblematicComponent />
      </ErrorBoundary>
      <p>Other content outside the boundary.</p>
    </div>
  );
}

export default App;

2. Error Handling in Event Handlers

Error Boundaries only catch errors that happen during rendering, in lifecycle methods, and in constructors. They do *not* catch errors inside event handlers. For errors within event handlers, a standard JavaScript try-catch block is the appropriate solution.

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

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

  const handleClick = () => {
    try {
      // Simulate an error, e.g., parsing invalid JSON
      const result = JSON.parse("invalid json"); 
      setData(result);
      setError(null);
    } catch (err) {
      console.error("Error in handleClick:", err);
      setError("Failed to process data: " + err.message);
    }
  };

  return (
    <div>
      <button onClick={handleClick}>Process Data</button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {data && <p>Data processed: {JSON.stringify(data)}</p>}
    </div>
  );
}

export default MyComponent;

3. Error Handling in Asynchronous Code

Similar to event handlers, errors in asynchronous operations (like fetch calls, axios requests, or Promises) are not caught by Error Boundaries. These should also be handled using try-catch with async/await or .catch() with Promises.

jsx
import React, { useEffect, useState } from 'react';

function DataLoader() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // Simulate an API call that might fail
        const response = await fetch('https://api.example.com/data-that-does-not-exist');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json();
        setData(json);
      } catch (err) {
        console.error("Error fetching data:", err);
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) return <p>Loading data...</p>;
  if (error) return <p style={{ color: 'red' }}>Error: {error}</p>;
  return <p>Data: {JSON.stringify(data)}</p>;
}

export default DataLoader;

4. Global Error Handling (for unhandled errors)

For unhandled JavaScript errors that fall outside of React's error boundary mechanism (e.g., script loading errors, unhandled promise rejections), you can use global event listeners on the window object:

  • window.onerror: Catches uncaught JavaScript errors.
  • window.onunhandledrejection: Catches unhandled Promise rejections.

5. Third-party Error Reporting Services

For production applications, integrating with specialized error reporting services is highly recommended. These services provide features like error aggregation, tracking, alerting, and detailed diagnostics. Popular options include Sentry, Bugsnag, and Rollbar.

6. Best Practices

  • Use Error Boundaries Strategically: Place them around logical blocks of your UI where errors might occur, rather than wrapping the entire App component with a single boundary. This allows for more granular fallback UIs.
  • Provide User-Friendly Fallbacks: Instead of a blank page or a cryptic error message, show a helpful message and optionally provide options to retry or navigate home.
  • Log Errors: Always log caught errors (e.g., to the console in development, or to an error reporting service in production) to aid debugging and monitoring.
  • Test Error Scenarios: Actively test how your application behaves when errors occur to ensure your error handling is robust.
  • Avoid Errors: While handling errors is important, the best approach is to prevent them through careful coding, validation, and testing.