Error Handling with Promises and Async/Await in JavaScript
Introduction
Error handling in asynchronous code is crucial for building reliable applications. Unlike synchronous code where you can use try-catch blocks, asynchronous operations require different approaches. Understanding how to properly handle errors in promises and async/await is essential for writing production-ready JavaScript.
In this article, you’ll learn comprehensive error handling techniques for both promises and async/await, including best practices and real-world patterns.
Promise Error Handling
Using .catch() Method
The .catch() method handles promise rejections.
// Basic promise rejection handling
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
console.error('Error:', error.message);
});
// Catching specific errors
fetch('/api/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => {
console.error('Failed to fetch users:', error);
});
Chaining Multiple .catch() Handlers
// Different catch handlers for different errors
fetch('/api/users')
.then(response => response.json())
.catch(error => {
console.error('Network error:', error);
throw error; // Re-throw to next catch
})
.then(data => processData(data))
.catch(error => {
console.error('Processing error:', error);
});
// Catching at different levels
Promise.resolve()
.then(() => {
throw new Error('Error in first then');
})
.catch(error => {
console.error('Caught:', error.message);
return 'recovered';
})
.then(result => {
console.log('Result:', result); // 'recovered'
});
Promise.prototype.finally()
The .finally() method runs regardless of whether the promise resolves or rejects.
// Cleanup with finally
let isLoading = true;
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => {
isLoading = false;
console.log('Request completed');
});
// Practical example: Loading state
function fetchWithLoading(url) {
console.log('Loading...');
return fetch(url)
.then(response => response.json())
.catch(error => {
console.error('Error:', error);
throw error;
})
.finally(() => {
console.log('Loading complete');
});
}
Async/Await Error Handling
Using try-catch Blocks
The try-catch block is the primary way to handle errors in async/await.
// Basic try-catch with async/await
async function fetchUsers() {
try {
const response = await fetch('/api/users');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching users:', error);
}
}
// Calling the async function
fetchUsers();
// With return value
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`User not found: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to get user:', error);
return null;
}
}
try-catch-finally with Async/Await
// Complete error handling pattern
async function processData(url) {
let data;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
data = await response.json();
console.log('Data processed:', data);
} catch (error) {
console.error('Error processing data:', error);
data = null;
} finally {
console.log('Processing complete');
}
return data;
}
// Practical example: Database operations
async function saveUserData(user) {
let connection;
try {
connection = await connectToDatabase();
await connection.save(user);
console.log('User saved successfully');
} catch (error) {
console.error('Failed to save user:', error);
throw error;
} finally {
if (connection) {
await connection.close();
}
}
}
Error Types and Handling
Distinguishing Error Types
// Handling different error types
async function robustFetch(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
if (error instanceof TypeError) {
console.error('Network error:', error.message);
} else if (error instanceof SyntaxError) {
console.error('Invalid JSON:', error.message);
} else {
console.error('Unknown error:', error);
}
}
}
// Custom error handling
async function fetchWithErrorHandling(url) {
try {
const response = await fetch(url);
if (response.status === 404) {
throw new Error('Resource not found');
}
if (response.status === 401) {
throw new Error('Unauthorized');
}
if (response.status === 500) {
throw new Error('Server error');
}
return await response.json();
} catch (error) {
if (error.message === 'Unauthorized') {
// Handle unauthorized
redirectToLogin();
} else if (error.message === 'Server error') {
// Handle server error
showErrorNotification('Server is down');
} else {
// Handle other errors
console.error(error);
}
}
}
Creating Custom Error Classes
// Custom error class
class APIError extends Error {
constructor(message, status, response) {
super(message);
this.name = 'APIError';
this.status = status;
this.response = response;
}
}
// Using custom error
async function fetchAPI(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new APIError(
`API request failed: ${response.statusText}`,
response.status,
response
);
}
return await response.json();
} catch (error) {
if (error instanceof APIError) {
console.error(`API Error ${error.status}: ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
Advanced Error Handling Patterns
Pattern 1: Retry Logic
// Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (i === maxRetries - 1) {
throw error; // Last attempt failed
}
// Wait before retrying
const delay = Math.pow(2, i) * 1000; // Exponential backoff
console.log(`Retry attempt ${i + 1} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
try {
const data = await fetchWithRetry('/api/data');
console.log(data);
} catch (error) {
console.error('Failed after retries:', error);
}
Pattern 2: Error Recovery
// Graceful error recovery
async function fetchUserWithFallback(userId) {
try {
// Try primary API
return await fetch(`/api/users/${userId}`).then(r => r.json());
} catch (error) {
console.warn('Primary API failed, trying fallback');
try {
// Try fallback API
return await fetch(`/api/backup/users/${userId}`).then(r => r.json());
} catch (fallbackError) {
console.error('Both APIs failed');
// Return cached data or default
return {
id: userId,
name: 'Unknown User',
cached: true
};
}
}
}
Pattern 3: Error Aggregation
// Collect multiple errors
async function processMultipleRequests(urls) {
const results = [];
const errors = [];
for (const url of urls) {
try {
const response = await fetch(url);
const data = await response.json();
results.push(data);
} catch (error) {
errors.push({
url,
error: error.message
});
}
}
return { results, errors };
}
// Usage
const { results, errors } = await processMultipleRequests([
'/api/users',
'/api/posts',
'/api/comments'
]);
if (errors.length > 0) {
console.warn('Some requests failed:', errors);
}
Pattern 4: Timeout Handling
// Fetch with timeout
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// Usage
try {
const data = await fetchWithTimeout('/api/data', 3000);
console.log(data);
} catch (error) {
console.error('Request failed:', error.message);
}
Practical Real-World Examples
Example 1: API Client with Error Handling
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`API request failed: ${error.message}`);
throw error;
}
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
// Usage
const api = new APIClient('https://api.example.com');
try {
const users = await api.get('/users');
console.log(users);
} catch (error) {
console.error('Failed to fetch users:', error);
}
Example 2: Form Submission with Error Handling
async function handleFormSubmit(event) {
event.preventDefault();
const form = event.target;
const submitButton = form.querySelector('button[type="submit"]');
const errorContainer = form.querySelector('.error-message');
try {
// Disable button and show loading state
submitButton.disabled = true;
submitButton.textContent = 'Submitting...';
errorContainer.textContent = '';
// Collect form data
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// Validate data
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
// Submit form
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Login failed');
}
const result = await response.json();
console.log('Login successful:', result);
// Redirect or update UI
window.location.href = '/dashboard';
} catch (error) {
// Display error message
errorContainer.textContent = error.message;
errorContainer.style.display = 'block';
console.error('Form submission error:', error);
} finally {
// Re-enable button
submitButton.disabled = false;
submitButton.textContent = 'Submit';
}
}
Example 3: Data Processing Pipeline
async function processDataPipeline(dataSource) {
try {
// Step 1: Fetch data
console.log('Fetching data...');
const rawData = await fetchData(dataSource);
// Step 2: Validate data
console.log('Validating data...');
const validatedData = validateData(rawData);
// Step 3: Transform data
console.log('Transforming data...');
const transformedData = await transformData(validatedData);
// Step 4: Save data
console.log('Saving data...');
const result = await saveData(transformedData);
console.log('Pipeline completed successfully');
return result;
} catch (error) {
console.error('Pipeline failed:', error);
// Log error details
console.error('Error type:', error.constructor.name);
console.error('Error message:', error.message);
console.error('Stack trace:', error.stack);
// Notify user
showErrorNotification('Data processing failed. Please try again.');
throw error;
}
}
async function fetchData(source) {
const response = await fetch(source);
if (!response.ok) throw new Error('Failed to fetch data');
return response.json();
}
function validateData(data) {
if (!Array.isArray(data)) {
throw new Error('Data must be an array');
}
return data;
}
async function transformData(data) {
return data.map(item => ({
...item,
processed: true,
timestamp: new Date()
}));
}
async function saveData(data) {
const response = await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to save data');
return response.json();
}
Common Mistakes to Avoid
Mistake 1: Not Handling Promise Rejections
// โ Wrong - Unhandled rejection
async function fetchData() {
const data = await fetch('/api/data').then(r => r.json());
return data;
}
// โ
Correct - Handle rejection
async function fetchData() {
try {
const data = await fetch('/api/data').then(r => r.json());
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
Mistake 2: Swallowing Errors Silently
// โ Wrong - Error is hidden
async function getData() {
try {
return await fetch('/api/data').then(r => r.json());
} catch (error) {
// Error is silently ignored
}
}
// โ
Correct - Log or handle error
async function getData() {
try {
return await fetch('/api/data').then(r => r.json());
} catch (error) {
console.error('Failed to get data:', error);
throw error; // Re-throw if needed
}
}
Mistake 3: Not Checking Response Status
// โ Wrong - Doesn't check if response is OK
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json(); // May parse error response
}
// โ
Correct - Check response status
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
Summary
Proper error handling in asynchronous code is essential:
- Use
.catch()for promise-based error handling - Use try-catch blocks with async/await for cleaner code
- Always use
.finally()for cleanup operations - Distinguish between different error types
- Implement retry logic for transient failures
- Create custom error classes for better error handling
- Always check response status in fetch requests
- Log errors appropriately for debugging
- Provide meaningful error messages to users
Related Resources
- MDN: Promise.prototype.catch()
- MDN: Promise.prototype.finally()
- MDN: Async/Await
- MDN: Error Handling
- MDN: Fetch API Error Handling
Next Steps
Continue your learning journey:
- Promises: Creation, Chaining, Resolution
- Async/Await: Modern Asynchronous Programming
- Promise Utilities: all, race, allSettled, any
- Event Loop and Microtasks
Comments