Mastering Asynchronous Error Handling Patterns in Modern JavaScript
Learn how to handle errors effectively in asynchronous JavaScript using promises, async/await, and other modern patterns to write robust, bug-free code.
Asynchronous programming is essential in modern JavaScript, especially when working with network requests, file operations, or timers. However, handling errors in asynchronous code can be tricky for beginners. This article covers common patterns to manage errors properly using promises, async/await syntax, and some practical tips.
Let's start by understanding why synchronous error handling (like try/catch) behaves differently with asynchronous code.
### Errors with Promises
When working with promises, errors are caught using the `.catch()` method. Here's a simple example:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Error fetching data:', error);
});If any promise in the chain rejects (for example, due to a network error), the `.catch()` handler will catch the error.
### Using async/await with try/catch
Async/await makes asynchronous code look more like synchronous code and allows us to use `try/catch` blocks to handle errors.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log('Data:', data);
} catch (error) {
console.error('Caught an error:', error);
}
}
fetchData();Here, any error thrown inside the `try` block will be caught in the `catch` block. This includes network errors and manual errors (like when the response is not ok).
### Handling Multiple Asynchronous Operations
Sometimes, you need to run multiple asynchronous operations simultaneously and handle their errors properly. `Promise.all` helps run promises in parallel but it rejects immediately if any one promise rejects.
const promise1 = fetch('https://api.example.com/data1');
const promise2 = fetch('https://api.example.com/data2');
Promise.all([promise1, promise2])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(datas => {
console.log('Both data:', datas);
})
.catch(error => {
console.error('Error in one of the promises:', error);
});If you want to handle errors individually without failing all promises together, use `Promise.allSettled`.
Promise.allSettled([promise1, promise2])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index + 1} succeeded with value:`, result.value);
} else {
console.warn(`Promise ${index + 1} failed with reason:`, result.reason);
}
});
});### Best Practices for Asynchronous Error Handling
- Always handle promise rejections to avoid uncaught errors. - Use `try/catch` around `await` expressions. - Check for HTTP response status before processing data. - Use `Promise.allSettled` when you need to know the outcome of all promises individually. - Consider adding fallback or retry logic if appropriate.
Mastering asynchronous error handling improves code reliability and helps you create better user experiences. Keep practicing these patterns, and you’ll find writing asynchronous JavaScript becomes much smoother!