Introduction
Asynchronous programming is a core part of modern JavaScript. Promises and async/await make async code easier to write and read, but error handling can still be confusing. Many bugs happen because promise errors are not handled correctly.
In simple words, if a promise fails and you do not catch the error, your application may crash, behave unexpectedly, or fail silently. In this article, you will learn the correct way to handle asynchronous errors in JavaScript promises using clear explanations and easy examples.
What Is an Asynchronous Error in JavaScript?
An asynchronous error occurs after an async operation starts, such as a network request, file read, or timer.
Example:
fetch('/api/data')
.then(response => response.json());
If the request fails and the error is not handled, JavaScript raises an unhandled promise rejection.
Handling Errors Using .catch()
The most basic and important rule is to always use .catch() when working with promises.
Example:
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Explanation:
This is the correct way to handle errors when using .then().
Returning Promises Correctly
A common mistake is forgetting to return a promise inside .then().
Wrong example:
fetch('/api/data')
.then(response => {
response.json();
})
.catch(error => console.error(error));
Correct example:
fetch('/api/data')
.then(response => {
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error));
Returning the promise ensures errors propagate correctly.
Handling Errors with async/await and try-catch
The recommended modern approach is using async/await with try-catch blocks.
Example:
async function loadData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Failed to load data:', error);
}
}
Explanation:
This approach is clean, readable, and easy to debug.
Throwing Errors Inside Promises
You can throw custom errors inside promises, and they will be caught by .catch() or try-catch.
Example:
async function getUser(userId) {
if (!userId) {
throw new Error('User ID is required');
}
return { id: userId, name: 'Rahul' };
}
Thrown errors behave like rejected promises.
Handling Multiple Promises Safely
When working with multiple promises, error handling becomes more important.
Example with Promise.all:
Promise.all([fetch('/api/a'), fetch('/api/b')])
.then(results => console.log(results))
.catch(error => console.error('One request failed:', error));
If any promise fails, .catch() is triggered.
Avoiding Unhandled Promise Rejections
Unhandled promise rejections happen when errors are not caught.
Bad example:
async function run() {
fetch('/api/data');
}
Good example:
async function run() {
try {
await fetch('/api/data');
} catch (error) {
console.error(error);
}
}
Always handle promises explicitly.
Global Error Handling (Last Resort)
You can listen for unhandled promise rejections globally.
Example:
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
});
This should be used only as a safety net, not as the main error-handling strategy.
Comparison Table: .then/.catch vs async/await
The table below helps you quickly understand the differences between the two common approaches.
| Aspect | .then() / .catch() | async / await |
|---|
| Readability | Can become nested | Clean and readable |
| Error handling | Uses .catch() | Uses try-catch |
| Debugging | Slightly harder | Easier |
| Beginner-friendly | Moderate | High |
| Recommended today | Less preferred | Yes |
Real-World Examples with Fetch API and Axios
Fetch API Example
async function loadUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error.message);
}
}
Explanation:
Axios Example
import axios from 'axios';
async function loadPosts() {
try {
const response = await axios.get('/api/posts');
console.log(response.data);
} catch (error) {
if (error.response) {
console.error('Server error:', error.response.status);
} else {
console.error('Network error:', error.message);
}
}
}
Axios automatically rejects promises for non-2xx responses, making error handling simpler.
Node.js vs Browser Error Handling
Error handling behavior differs slightly between Node.js and browsers.
Browser
window.addEventListener('unhandledrejection', event => {
console.error('Browser unhandled rejection:', event.reason);
});
Node.js
process.on('unhandledRejection', (reason) => {
console.error('Node.js unhandled rejection:', reason);
});
In production, global handlers should log errors and allow graceful shutdown.
Testing Asynchronous Errors
Testing async error handling ensures your code behaves correctly during failures.
Example using a test framework:
async function fetchData() {
throw new Error('API failed');
}
test('handles async error', async () => {
await expect(fetchData()).rejects.toThrow('API failed');
});
Best practices for testing async errors:
Test both success and failure paths
Mock failed API responses
Ensure promises are awaited in tests
Best Practices for Promise Error Handling
To avoid async error issues:
Always use .catch() or try-catch
Return promises correctly
Handle errors close to where they happen
Log meaningful error messages
Avoid empty catch blocks
These practices make your JavaScript applications more stable.
Summary
The correct way to handle asynchronous errors in JavaScript promises is to always catch them using .catch() or try-catch with async/await. Properly returning promises, throwing meaningful errors, and avoiding unhandled rejections are key to building reliable applications. With consistent error-handling practices, JavaScript async code becomes easier to maintain, debug, and scale.