Not handling API failures leading to unhandled errors

Loading

Failing to properly handle API failures is a common oversight that can lead to broken user experiences and uncaught errors in React applications. Here’s how to implement robust error handling for API calls.

The Core Problem

Unhandled API failures manifest as:

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user') // ❌ No error handling
      .then(response => response.json())
      .then(setUser);
  }, []);

  return <div>{user.name}</div>; // ❌ Potential TypeError
}

Correct Implementation

1. Basic Error Handling

function UserProfile() {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

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

    fetchUser();
  }, []);

  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;
  return <div>{user.name}</div>;
}

Common Scenarios and Solutions

❌ Ignoring Failed HTTP Status Codes

fetch('/api/user')
  .then(response => response.json()) // ❌ Fails silently on 404/500
  .then(setUser);

Fix: Check response status

fetch('/api/user')
  .then(response => {
    if (!response.ok) throw new Error('Request failed');
    return response.json();
  })
  .then(setUser)
  .catch(handleError);

❌ Unhandled Promise Rejections

useEffect(() => {
  fetchData(); // ❌ Uncaught promise if rejected
}, []);

Fix: Proper async/await with try/catch

useEffect(() => {
  const loadData = async () => {
    try {
      await fetchData();
    } catch (err) {
      handleError(err);
    }
  };
  loadData();
}, []);

Advanced Error Handling Patterns

1. Centralized Error Handling

// apiClient.js
export async function apiClient(url, options = {}) {
  const response = await fetch(url, options);

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

  return response.json();
}

// Usage
try {
  const user = await apiClient('/api/user');
} catch (err) {
  if (err.status === 404) {
    // Handle not found
  } else {
    // Handle other errors
  }
}

2. Error Boundary Component

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

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

  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }

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

// Usage
<ErrorBoundary>
  <UserProfile />
</ErrorBoundary>

3. Retry Mechanism

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

  useEffect(() => {
    let attempt = 0;

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

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

  return { data, error };
}

Best Practices

  1. Always check response.ok for fetch requests
  2. Use try/catch with async/await
  3. Provide user feedback for failures
  4. Log errors to monitoring services
  5. Implement retries for transient failures
  6. Use Error Boundaries for catastrophic failures
  7. Type API responses with TypeScript

Real-World Example

function useUserProfile(userId) {
  const [state, setState] = useState({
    user: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setState(prev => ({ ...prev, loading: true }));

        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(
            `Failed to fetch user: ${response.status}`
          );
        }

        const user = await response.json();
        setState({ user, loading: false, error: null });
      } catch (err) {
        setState({
          user: null,
          loading: false,
          error: err.message
        });
        logError(err);
      }
    };

    fetchUser();
  }, [userId]);

  return state;
}

function UserProfile({ userId }) {
  const { user, loading, error } = useUserProfile(userId);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return (
    <div>
      <h1>{user.name}</h1>
      {/* Profile content */}
    </div>
  );
}

Key Takeaways

  1. Never assume API calls will succeed – always handle errors
  2. Provide clear feedback to users when things go wrong
  3. Distinguish between different error types (network, 4xx, 5xx)
  4. Implement defensive coding practices
  5. Monitor API failures in production

Proper API error handling creates more resilient applications and better user experiences when things don’t go as planned.

Leave a Reply

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