Universal Error Handling in React: ErrorBoundary and Beyond

Oscar Bustos

Implementing robust error handling in React applications using ErrorBoundaries and modern patterns

6 min read

Basic ErrorBoundary Implementation

A foundation for catching and handling React errors:

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

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

  componentDidCatch(error, errorInfo) {
    // Log to error reporting service
    console.error('ErrorBoundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <DefaultErrorUI error={this.state.error} />;
    }

    return this.props.children;
  }
}

// Usage
const App = () => {
  return (
    <ErrorBoundary fallback={<CustomErrorPage />}>
      <MainContent />
    </ErrorBoundary>
  );
};

Handling Async Errors

Catch errors in async operations and propagate them to ErrorBoundary:

const useAsyncError = () => {
  const [, setError] = useState();
  return useCallback(
    error => {
      setError(() => {
        throw error;
      });
    },
    []
  );
};

const AsyncComponent = () => {
  const throwError = useAsyncError();

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const data = await response.json();
        // Use data...
      } catch (error) {
        throwError(error);
      }
    };

    fetchData();
  }, []);

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

Reusable Error Boundary Hook

Create a custom hook for error boundaries:

const useErrorBoundary = () => {
  const [error, setError] = useState(null);

  if (error) {
    throw error;
  }

  return setError;
};

const ComponentWithError = () => {
  const throwError = useErrorBoundary();

  const handleClick = () => {
    try {
      riskyOperation();
    } catch (error) {
      throwError(error);
    }
  };

  return <button onClick={handleClick}>Trigger Error</button>;
};

Network Error Handling

Handle API errors gracefully:

const useFetch = (url) => {
  const [state, setState] = useState({
    data: null,
    error: null,
    loading: true
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        setState({ data, error: null, loading: false });
      } catch (error) {
        setState({ data: null, error, loading: false });
      }
    };

    fetchData();
  }, [url]);

  return state;
};

const UserProfile = ({ userId }) => {
  const { data, error, loading } = useFetch(`/api/users/${userId}`);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;

  return <ProfileData user={data} />;
};

Form Error Handling

Handle form validation and submission errors:

const FormWithErrorHandling = () => {
  const [errors, setErrors] = useState({});
  
  const validate = (values) => {
    const errors = {};
    
    if (!values.email) {
      errors.email = 'Required';
    } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
      errors.email = 'Invalid email address';
    }
    
    return errors;
  };

  const handleSubmit = async (values) => {
    try {
      const errors = validate(values);
      
      if (Object.keys(errors).length > 0) {
        setErrors(errors);
        return;
      }

      await submitForm(values);
    } catch (error) {
      setErrors({ submit: error.message });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" />
      {errors.email && <span className="error">{errors.email}</span>}
      
      {errors.submit && (
        <div className="error-banner">{errors.submit}</div>
      )}
      
      <button type="submit">Submit</button>
    </form>
  );
};

Global Error Handler

Catch unhandled errors application-wide:

const GlobalErrorHandler = () => {
  useEffect(() => {
    const handler = (event) => {
      console.error(
        'Unhandled error:',
        event.error?.message || 'Unknown error'
      );
      
      // Prevent default browser error overlay
      event.preventDefault();
      
      // Show user-friendly error message
      // You could use a global state management solution here
      showErrorNotification(
        'Something went wrong. Please try again later.'
      );
    };

    window.addEventListener('error', handler);
    window.addEventListener('unhandledrejection', handler);

    return () => {
      window.removeEventListener('error', handler);
      window.removeEventListener('unhandledrejection', handler);
    };
  }, []);

  return null;
};

// Usage in root component
const App = () => (
  <>
    <GlobalErrorHandler />
    <RestOfApp />
  </>
);

Key Error Handling Strategies

  1. Use ErrorBoundary for component rendering errors
  2. Implement async error handling with custom hooks
  3. Handle network errors in data fetching operations
  4. Validate forms and handle submission errors gracefully
  5. Set up global error handling for unhandled exceptions

Is it a match?

If you need help or advice, feel free to drop us a message via social media or email

Contact me