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
- Always check response.ok for fetch requests
- Use try/catch with async/await
- Provide user feedback for failures
- Log errors to monitoring services
- Implement retries for transient failures
- Use Error Boundaries for catastrophic failures
- 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
- Never assume API calls will succeed – always handle errors
- Provide clear feedback to users when things go wrong
- Distinguish between different error types (network, 4xx, 5xx)
- Implement defensive coding practices
- Monitor API failures in production
Proper API error handling creates more resilient applications and better user experiences when things don’t go as planned.