Code To Learn logo

Code To Learn

M3: Effects & Data

L5: Error Handling

Handle API errors gracefully and provide user-friendly error messages

What You'll Learn

Network requests can fail! In this lesson, you'll learn to handle errors gracefully:

  • Add error state to track failures
  • Use try/catch blocks for error handling
  • Create user-friendly error UI
  • Implement retry functionality
  • Learn error handling best practices

Why Error Handling Matters

Real-world scenarios where APIs fail:

  • 🌐 Network connectivity issues
  • 🔥 Server is down or overloaded
  • ⏰ Request timeout
  • 🔒 Authentication/permission errors
  • 📉 Rate limiting
  • 🐛 Backend bugs

Without error handling:

  • App crashes or shows broken UI
  • Users see blank screens
  • No way to recover
  • Terrible user experience

With error handling:

  • Clear error messages
  • Retry options
  • Graceful degradation
  • Professional user experience

The Error Pattern

Standard Error Handling Pattern
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); 

useEffect(() => {
  const fetchData = async () => {
    setIsLoading(true);
    setError(null); // Clear previous errors
    
    try {
      const result = await apiCall();
      setData(result);
    } catch (err) { 
      setError(err.message); 
      console.error('Fetch error:', err); 
    } finally {
      setIsLoading(false);
    }
  };
  
  fetchData();
}, []);

// Conditional rendering
if (error) return <ErrorDisplay error={error} onRetry={fetchData} />;
if (isLoading) return <Spinner />;
return <DataDisplay data={data} />;

Step 1: Create Error UI Component

Create ErrorMessage Component

src/components/ErrorMessage.jsx
import { AlertCircle, RefreshCw } from 'lucide-react';

export function ErrorMessage({ 
  title = 'Something went wrong', 
  message, 
  onRetry 
}) {
  return (
    <div className="flex flex-col items-center justify-center p-8 text-center">
      {/* Error Icon */}
      <div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
        <AlertCircle className="w-8 h-8 text-red-600" />
      </div>

      {/* Error Title */}
      <h2 className="text-2xl font-bold text-gray-900 mb-2">
        {title}
      </h2>

      {/* Error Message */}
      <p className="text-gray-600 mb-6 max-w-md">
        {message || 'We encountered an error loading the data. Please try again.'}
      </p>

      {/* Retry Button */}
      {onRetry && (
        <button
          onClick={onRetry}
          className="
            flex items-center gap-2 
            px-6 py-3 
            bg-blue-600 text-white 
            rounded-lg font-medium
            hover:bg-blue-700 
            transition-colors
          "
        >
          <RefreshCw className="w-5 h-5" />
          Try Again
        </button>
      )}

      {/* Help Text */}
      <p className="text-sm text-gray-500 mt-4">
        If this problem persists, please contact support.
      </p>
    </div>
  );
}

Component features:

  • title prop - Customizable error title
  • message prop - Specific error description
  • onRetry prop - Optional retry callback
  • Icons - Uses lucide-react for visual feedback
  • Styling - Clean, professional error UI

Install lucide-react (if needed)

Terminal
npm install lucide-react

Lucide React provides beautiful, consistent icons for your UI.

Step 2: Add Error State to HomePage

Update HomePage State

src/pages/HomePage.jsx
import { useState, useEffect } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { Spinner } from '@/components/Spinner';
import { ErrorMessage } from '@/components/ErrorMessage'; 
import { getAllListings } from '@/api';

export function HomePage() {
  const [listings, setListings] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null); 
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ checkIn: null, checkOut: null });
  const [guests, setGuests] = useState(1);

  // ... rest of component
}

Update useEffect with Error Handling

src/pages/HomePage.jsx (useEffect)
useEffect(() => {
  const fetchListings = async () => {
    setIsLoading(true);
    setError(null); // Clear previous errors
    
    try {
      const data = await getAllListings();
      setListings(data);
      console.log('Fetched listings:', data);
    } catch (err) { 
      console.error('Failed to fetch listings:', err); 
      setError(err.message || 'Failed to load listings'); 
    } finally {
      setIsLoading(false);
    }
  };

  fetchListings();
}, []);

What changed:

  1. setError(null) - Clears previous errors before fetching
  2. catch (err) - Catches any errors from API call
  3. setError(err.message) - Stores error message in state
  4. finally - Always clears loading state

Add Error Rendering

src/pages/HomePage.jsx (return statement)
export function HomePage() {
  // ... state and useEffect

  // Filter listings
  const filteredListings = listings.filter(listing => {
    // ... filter logic
  });

  // Error state
  if (error) { 
    return ( 
      <div className="min-h-screen bg-gray-50"> // [!code ++]
        <div className="container mx-auto px-4 py-8"> // [!code ++]
          <h1 className="text-3xl font-bold mb-8"> // [!code ++]
            Find Your Perfect Stay // [!code ++]
          </h1> // [!code ++]
          <ErrorMessage
            title="Unable to Load Listings"
            message={error} 
            onRetry={() => window.location.reload()} 
          /> // [!code ++]
        </div> // [!code ++]
      </div> 
    ); 
  } 

  // Loading state
  if (isLoading) {
    return (
      <div className="min-h-screen bg-gray-50">
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">
            Find Your Perfect Stay
          </h1>
          <Spinner 
            size="large"
            message="Loading amazing stays..."
          />
        </div>
      </div>
    );
  }

  // Main content
  return (
    // ... normal UI
  );
}

Render priority:

  1. ✋ If error → Show error message
  2. ⏳ Else if loading → Show spinner
  3. ✅ Else → Show data

Complete Updated Component

src/pages/HomePage.jsx (Complete with Error Handling)
import { useState, useEffect } from 'react';
import { ListingList } from '@/components/ListingList';
import { ListingFilters } from '@/components/ListingFilters';
import { Spinner } from '@/components/Spinner';
import { ErrorMessage } from '@/components/ErrorMessage';
import { getAllListings } from '@/api';

export function HomePage() {
  // State
  const [listings, setListings] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [search, setSearch] = useState('');
  const [dates, setDates] = useState({ checkIn: null, checkOut: null });
  const [guests, setGuests] = useState(1);

  // Fetch listings on mount
  useEffect(() => {
    const fetchListings = async () => {
      setIsLoading(true);
      setError(null);
      
      try {
        const data = await getAllListings();
        setListings(data);
        console.log('Fetched listings:', data);
      } catch (err) {
        console.error('Failed to fetch listings:', err);
        setError(err.message || 'Failed to load listings');
      } finally {
        setIsLoading(false);
      }
    };

    fetchListings();
  }, []);

  // Filter listings
  const filteredListings = listings.filter(listing => {
    const matchesSearch = 
      search === '' ||
      listing.title.toLowerCase().includes(search.toLowerCase()) ||
      listing.location.toLowerCase().includes(search.toLowerCase());

    const matchesGuests = listing.maxGuests >= guests;
    const matchesDates = true;

    return matchesSearch && matchesGuests && matchesDates;
  });

  // Error state
  if (error) {
    return (
      <div className="min-h-screen bg-gray-50">
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">
            Find Your Perfect Stay
          </h1>
          <ErrorMessage 
            title="Unable to Load Listings"
            message={error}
            onRetry={() => window.location.reload()}
          />
        </div>
      </div>
    );
  }

  // Loading state
  if (isLoading) {
    return (
      <div className="min-h-screen bg-gray-50">
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">
            Find Your Perfect Stay
          </h1>
          <Spinner 
            size="large"
            message="Loading amazing stays..."
          />
        </div>
      </div>
    );
  }

  // Main content
  return (
    <div className="min-h-screen bg-gray-50">
      <div className="container mx-auto px-4 py-8">
        <h1 className="text-3xl font-bold mb-8">Find Your Perfect Stay</h1>
        
        <ListingFilters 
          search={search}
          onSearchChange={setSearch}
          dates={dates}
          onDatesChange={setDates}
          guests={guests}
          onGuestsChange={setGuests}
        />

        <div className="mt-8">
          <p className="text-gray-600 mb-4">
            Showing {filteredListings.length} of {listings.length} listings
          </p>
          
          <ListingList listings={filteredListings} />
        </div>
      </div>
    </div>
  );
}

Advanced Error Handling Patterns

Smart Retry with Exponential Backoff

Retry with Delay
const [retryCount, setRetryCount] = useState(0);
const MAX_RETRIES = 3;

useEffect(() => {
  const fetchWithRetry = async (attempt = 0) => {
    setIsLoading(true);
    setError(null);

    try {
      const data = await getAllListings();
      setListings(data);
      setRetryCount(0); // Reset on success
    } catch (err) {
      if (attempt < MAX_RETRIES) {
        // Exponential backoff: 1s, 2s, 4s
        const delay = Math.pow(2, attempt) * 1000;
        console.log(`Retry ${attempt + 1} in ${delay}ms...`);
        
        setTimeout(() => {
          setRetryCount(attempt + 1);
          fetchWithRetry(attempt + 1);
        }, delay);
      } else {
        setError(err.message);
      }
    } finally {
      setIsLoading(false);
    }
  };

  fetchWithRetry();
}, []);

Features:

  • Automatically retries up to 3 times
  • Increasing delays between attempts
  • Shows final error after max retries

Handle Different Error Types

Specific Error Messages
try {
  const data = await getAllListings();
  setListings(data);
} catch (err) {
  let errorMessage = 'An unexpected error occurred';

  // Network errors
  if (err.message.includes('Network')) {
    errorMessage = 'Network connection lost. Please check your internet.';
  }
  // Timeout errors
  else if (err.message.includes('timeout')) {
    errorMessage = 'Request timed out. The server is taking too long to respond.';
  }
  // Server errors
  else if (err.message.includes('500')) {
    errorMessage = 'Server error. Our team has been notified.';
  }
  // Not found
  else if (err.message.includes('404')) {
    errorMessage = 'Data not found. It may have been removed.';
  }
  // Permission errors
  else if (err.message.includes('403') || err.message.includes('401')) {
    errorMessage = 'You don\'t have permission to view this content.';
  }

  setError(errorMessage);
}

Show Fallback Content on Error

Partial Failure Handling
// Don't show error UI, show cached/default data instead
if (error && listings.length === 0) {
  return <ErrorMessage error={error} />;
}

return (
  <div>
    {error && (
      <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
        <p className="text-yellow-700">
          ⚠️ Some data may be outdated. {error}
        </p>
      </div>
    )}
    
    <ListingList listings={listings} />
  </div>
);

Use case: Show cached data even if API fails.

React Error Boundaries

Catch errors in component tree:

src/components/ErrorBoundary.jsx
import React from 'react';
import { ErrorMessage } from './ErrorMessage';

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

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

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <ErrorMessage
          title="Something went wrong"
          message={this.state.error?.message}
          onRetry={() => this.setState({ hasError: false })}
        />
      );
    }

    return this.props.children;
  }
}

Usage:

src/App.jsx
import { ErrorBoundary } from './components/ErrorBoundary';

function App() {
  return (
    <ErrorBoundary>
      <HomePage />
    </ErrorBoundary>
  );
}

Error Handling Best Practices

Testing Error Handling

Test with Forced Error

Update your API to force an error:

src/api/listings.js (Temporary)
export const getAllListings = async (options = {}) => {
  const response = await mockApiCall(mockListings, {
    delayMs: 1000,
    shouldFail: true, // Force error
    ...options
  });
  return response.data;
};

You should see the error UI with retry button.

Test Random Errors

Use the error rate option:

Random Errors (5%)
const response = await mockApiCall(mockListings, {
  errorRate: 0.05, // 5% chance of error
});

Refresh multiple times to see errors occasionally.

Test Retry Button

  1. Force an error
  2. Click "Try Again" button
  3. Should clear error and fetch again

Key Takeaways

Robust error handling!

Before:

  • ❌ Silent failures
  • ❌ Blank screens on error
  • ❌ No recovery options

After:

  • ✅ Clear error messages
  • ✅ User-friendly UI
  • ✅ Retry functionality
  • ✅ Professional error handling

What you learned:

  • try/catch/finally pattern
  • Error state management
  • Creating error UI components
  • Error handling best practices
  • Testing error scenarios

What's Next?

Your app now handles loading and errors, but there's one more critical issue: race conditions. In the next lesson, you'll:

  • 🏁 Learn what race conditions are
  • 🚫 Use AbortController to cancel requests
  • 🧹 Implement cleanup functions
  • 🔒 Prevent stale data updates

Let's make your app bulletproof! 🛡️