![]()
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
- User Experience: Prevents blank screens or broken UIs
- Debugging: Makes issues easier to diagnose
- Stability: Prevents entire app crashes
- 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
- Always Catch Errors: Never let API failures go unhandled
- Set Proper State: Track loading, error, and data states
- User Feedback: Show helpful error messages
- Log Errors: Send to error tracking service
- Type Safety: Validate API responses
Common API Error Types to Handle
- Network Errors: Failed connections
- HTTP Errors: 4xx/5xx responses
- Invalid Data: Malformed responses
- Authentication Issues: 401/403 responses
- 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
- Assuming Success:
// ❌ Dangerous assumption
const data = await response.json();
setData(data.items); // What if items doesn't exist?
- Ignoring Response Status:
// ❌ Missing status check
const response = await fetch(url);
const data = await response.json(); // Fails on 404/500
- Not Aborting on Unmount:
useEffect(() => {
fetch(url).then(/* ... */); // ❌ Potential memory leak
}, []);
- 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.
