Not handling API failures leading to unhandled errors

Loading

Proper API Error Handling in React Applications

A critical aspect of robust React development is properly handling API failures to prevent uncaught errors and provide a good user experience.

The Problem: Unhandled API Errors

// ❌ Dangerous API call with no error handling
useEffect(() => {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => setData(data)); // Crashes if API fails
}, []);

Why Proper Error Handling Matters

  1. User Experience: Prevents blank screens or broken UIs
  2. Debugging: Makes issues easier to diagnose
  3. Stability: Prevents entire app crashes
  4. Monitoring: Enables proper error tracking

Correct Implementation Patterns

1. Basic Try/Catch with Async/Await

useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Network response was not ok');
      setData(await response.json());
    } catch (error) {
      console.error('Fetch error:', error);
      setError(error.message);
    }
  };

  fetchData();
}, []);

2. With Loading States

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        setData(await response.json());
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) return <Loader />;
  if (error) return <Error message={error} />;
  return <DataView data={data} />;
}

3. With Retry Mechanism

function useApiWithRetry(url, retries = 3) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [attempt, setAttempt] = useState(0);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('API request failed');
        setData(await response.json());
      } catch (err) {
        if (attempt < retries) {
          setTimeout(() => setAttempt(a => a + 1), 1000 * attempt);
        } else {
          setError(err.message);
        }
      }
    };

    fetchData();
  }, [url, attempt, retries]);

  return { data, error };
}

Best Practices

  1. Always Catch Errors: Never let API failures go unhandled
  2. Set Proper State: Track loading, error, and data states
  3. User Feedback: Show helpful error messages
  4. Log Errors: Send to error tracking service
  5. Type Safety: Validate API responses

Common API Error Types to Handle

  1. Network Errors: Failed connections
  2. HTTP Errors: 4xx/5xx responses
  3. Invalid Data: Malformed responses
  4. Authentication Issues: 401/403 responses
  5. Rate Limiting: 429 responses

Advanced Patterns

1. Custom API Hook

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

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        const response = await fetch(url, {
          signal: controller.signal
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        if (error.name !== 'AbortError') {
          setState({ data: null, loading: false, error: error.message });
        }
      }
    };

    fetchData();

    return () => controller.abort();
  }, [url]);

  return state;
}

2. Error Boundary Wrapper

class ApiErrorBoundary extends React.Component {
  state = { hasError: false };

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

  render() {
    if (this.state.hasError) {
      return <FallbackUI error={this.state.error} />;
    }
    return this.props.children;
  }
}

// Usage
<ApiErrorBoundary>
  <DataComponent />
</ApiErrorBoundary>

3. Response Validation

interface User {
  id: string;
  name: string;
  email: string;
}

function isValidUser(data: any): data is User {
  return (
    typeof data.id === 'string' &&
    typeof data.name === 'string' &&
    typeof data.email === 'string'
  );
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  if (!isValidUser(data)) {
    throw new Error('Invalid user data received');
  }

  return data;
}

Common Pitfalls

  1. Assuming Success:
   // ❌ Dangerous assumption
   const data = await response.json();
   setData(data.items); // What if items doesn't exist?
  1. Ignoring Response Status:
   // ❌ Missing status check
   const response = await fetch(url);
   const data = await response.json(); // Fails on 404/500
  1. Not Aborting on Unmount:
   useEffect(() => {
     fetch(url).then(/* ... */); // ❌ Potential memory leak
   }, []);
  1. Overwriting State During Errors:
   try {
     const newData = await fetchData();
     setData(newData);
   } catch {
     setData(null); // ❌ Loses previous good data
   }

Monitoring and Logging

// With error tracking service
try {
  // API call
} catch (error) {
  logErrorToService(error);
  setError(error.message);
}

// Or with boundary
componentDidCatch(error, info) {
  logErrorToService(error, info);
}

Remember: Proper API error handling should:

  • Prevent crashes
  • Provide user feedback
  • Maintain application state
  • Enable debugging
  • Support recovery options

Always assume your API calls can fail and handle those cases gracefully.

Leave a Reply

Your email address will not be published. Required fields are marked *