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
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
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:
titleprop - Customizable error titlemessageprop - Specific error descriptiononRetryprop - Optional retry callback- Icons - Uses lucide-react for visual feedback
- Styling - Clean, professional error UI
Install lucide-react (if needed)
npm install lucide-reactLucide React provides beautiful, consistent icons for your UI.
Step 2: Add Error State to HomePage
Update HomePage State
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
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:
setError(null)- Clears previous errors before fetchingcatch (err)- Catches any errors from API callsetError(err.message)- Stores error message in statefinally- Always clears loading state
Add Error Rendering
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:
- ✋ If error → Show error message
- ⏳ Else if loading → Show spinner
- ✅ Else → Show data
Complete Updated Component
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
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
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
// 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:
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:
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:
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:
const response = await mockApiCall(mockListings, {
errorRate: 0.05, // 5% chance of error
});Refresh multiple times to see errors occasionally.
Test Retry Button
- Force an error
- Click "Try Again" button
- 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! 🛡️