![]()
Proper Error Handling for Asynchronous Operations in React
Forgetting to wrap asynchronous calls in try/catch blocks is a common mistake that can lead to uncaught promise rejections and poor error handling in your React applications. Here’s how to properly handle asynchronous operations.
The Problem: Unhandled Promise Rejections
// ❌ Dangerous - uncaught errors
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);
Correct Approaches
1. Basic try/catch with async/await
// ✅ Proper error handling
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
setData(data);
} catch (error) {
console.error('Fetch error:', error);
setError(error.message);
}
};
fetchData();
}, []);
2. Error Boundaries for Component-Level Errors
// ErrorBoundary.js
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
3. Centralized Error Handling
// api.js
export async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
throw error; // Re-throw for component handling
}
}
// Component
useEffect(() => {
const loadData = async () => {
try {
const data = await fetchData();
setData(data);
} catch (error) {
setError(error.message);
}
};
loadData();
}, []);
Best Practices
- Always wrap async operations in try/catch
- Handle both network errors and API errors (check response.ok)
- Provide user feedback for failed operations
- Log errors to your error tracking service
- Consider retry mechanisms for transient failures
// With retry logic
const fetchWithRetry = async (url, retries = 3) => {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Bad response');
return await response.json();
} catch (error) {
if (retries <= 0) throw error;
await new Promise(resolve => setTimeout(resolve, 1000));
return fetchWithRetry(url, retries - 1);
}
};
Common Async Patterns in React
1. Data Fetching in Components
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('User not found');
setUser(await response.json());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <Loader />;
if (error) return <Error message={error} />;
return <Profile user={user} />;
}
2. Form Submission Handling
async function handleSubmit(event) {
event.preventDefault();
setSubmitting(true);
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Submission failed');
}
const result = await response.json();
onSuccess(result);
} catch (error) {
setSubmissionError(error.message);
} finally {
setSubmitting(false);
}
}
Advanced Error Handling Techniques
1. Custom Hook for Async Operations
function useAsync(asyncFn, immediate = true) {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(async (...args) => {
setStatus('pending');
try {
const result = await asyncFn(...args);
setValue(result);
setStatus('success');
return result;
} catch (error) {
setError(error);
setStatus('error');
throw error;
}
}, [asyncFn]);
useEffect(() => {
if (immediate) execute();
}, [execute, immediate]);
return { execute, status, value, error };
}
// Usage
const { status, value, error } = useAsync(() => fetchData(url));
2. Global Error Handling
// Error context
const ErrorContext = createContext();
function ErrorProvider({ children }) {
const [error, setError] = useState(null);
const handleError = useCallback((error) => {
console.error(error);
setError(error.message);
setTimeout(() => setError(null), 5000);
}, []);
return (
<ErrorContext.Provider value={{ error, handleError }}>
{error && <Notification message={error} />}
{children}
</ErrorContext.Provider>
);
}
// Usage in components
function UserComponent() {
const { handleError } = useContext(ErrorContext);
const fetchUser = async () => {
try {
// ... async operation
} catch (error) {
handleError(error);
}
};
}
Remember that proper error handling:
- Prevents app crashes from uncaught exceptions
- Provides better user experience with meaningful feedback
- Helps with debugging by properly logging errors
- Makes your application more robust and maintainable
Always wrap asynchronous operations in try/catch blocks and consider implementing multiple layers of error handling (component-level, context-level, and error boundaries) for comprehensive coverage.
