Error handling in Promise-based and async/await code has its own set of rules and gotchas that differ meaningfully from synchronous try/catch โ an error thrown inside a .then() callback, for instance, doesn't behave the way many developers initially expect.
1Promise Error Handling Patterns | JavaScript Tutorial - In-Depth Guide Part 1
Throwing an error inside a .then() callback doesn't crash your program โ it's automatically converted into a rejected Promise, which the next .catch() in the chain receives.
fetchUser()
.then((user) => {
if (!user.verified) throw new Error('User not verified');
return user;
})
.catch((err) => console.error(err.message)); // catches the thrown error tooThrows Become Rejections
2Promise Error Handling Patterns | JavaScript Tutorial - In-Depth Guide Part 2
A single .catch() at the end of a chain handles a rejection from ANY earlier .then() in that chain โ you rarely need a .catch() after every single step.
step1()
.then(step2)
.then(step3)
.catch((err) => {
// catches a failure from step1, step2, OR step3
});One catch() for the Whole Chain
3Promise Error Handling Patterns | JavaScript Tutorial - In-Depth Guide Part 3
With async/await, a rejected awaited promise throws an actual exception at that line, so ordinary try/catch works exactly as it does for synchronous code.
async function loadUser() {
try {
const user = await fetchUser();
return user;
} catch (err) {
console.error('Failed to load user:', err.message);
}
}try/catch with async/await
4Promise Error Handling Patterns | JavaScript Tutorial - In-Depth Guide Part 4
A common bug: forgetting to await a promise-returning function call inside a try block means its rejection happens outside the try/catch's ability to intercept it.
async function broken() {
try {
fetchUser(); // missing await!
} catch (err) {
// never runs โ the rejection happens after this function has already returned
}
}The Missing await Bug
5Promise Error Handling Patterns | JavaScript Tutorial - In-Depth Guide Part 5
When handling multiple promises together, choose Promise.all() (fail-fast) versus Promise.allSettled() (every outcome) deliberately based on whether partial failure is acceptable โ this decision directly shapes your error-handling code.
// All-or-nothing error handling:
try {
const [a, b] = await Promise.all([taskA(), taskB()]);
} catch (err) {
// one failure loses both results
}
// Per-item error handling:
const results = await Promise.allSettled([taskA(), taskB()]);
results.forEach((r) => r.status === 'rejected' && logFailure(r.reason));Choosing the Right Combinator
6Step-by-Step Breakdown
Throwing an error inside a .then() callback doesn't crash your program โ it's automatically converted into a rejected Promise, which the next .catch() in the chain receives.
Checkpoint: If you throw an error inside a .then() callback, does it crash the program immediately?
- โYes, thrown errors inside .then() always crash the script
- โNo, it's converted into a rejected Promise caught by the next .catch()
A single .catch() at the end of a chain handles a rejection from ANY earlier .then() in that chain โ you rarely need a .catch() after every single step.
With async/await, a rejected awaited promise throws an actual exception at that line, so ordinary try/catch works exactly as it does for synchronous code.
A common bug: forgetting to await a promise-returning function call inside a try block means its rejection happens outside the try/catch's ability to intercept it.
Checkpoint: If you forget to await a promise-returning call inside a try block, will its eventual rejection be caught by that try/catch?
- โYes, try/catch always catches it regardless of await
- โNo, the rejection happens after the try block has already finished
When handling multiple promises together, choose Promise.all() (fail-fast) versus Promise.allSettled() (every outcome) deliberately based on whether partial failure is acceptable โ this decision directly shapes your error-handling code.
Next, we'll explore 'Reading Stack Traces'.
Level Up ๐
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Ensure Every Step of an Async User Flow Has Reachable Error Feedback
In a multi-step async flow (like checkout), verify each awaited step's catch block actually surfaces an accessible error message tied to the relevant step, rather than a single generic catch-all that leaves users unsure which part of the process failed.
SEO Implications
- 1
No Direct SEO Effect
Promise error handling is an application-correctness concern; SEO relevance is limited to preventing broken async data flows behind rendered content.
Best Practices
Always await Promise-Returning Calls Inside a try Block You Expect to Catch Their Errors
A missing await causes the rejection to escape the try/catch entirely, since the function call returns immediately without the try block ever seeing the eventual failure.
Place a Single catch() (or try/catch) at the Appropriate Granularity for Your Error-Handling Needs
Don't scatter redundant .catch() calls after every single .then() unless each step genuinely needs different recovery logic โ one well-placed catch usually suffices for a whole chain.
Frequent Bugs
Calling an async function without awaiting it inside a try block, expecting its later rejection to be caught by that surrounding try/catch, when in reality it becomes an unhandled rejection.
Add the missing `await` before the promise-returning call so its rejection is properly funneled into the try/catch as a real thrown exception at that line.
Attaching a .catch() partway through a .then() chain that swallows an error silently (with no re-throw), causing later .then() steps to run as if nothing failed, using stale/undefined data.
Either handle the error fully and provide valid fallback data for later steps, or re-throw it if the chain should not continue after a failure at that point.
Real-World Examples
Correctly Handling a Multi-Step Async Checkout Flow
A checkout process needed to validate a cart, charge a payment method, and create an order, with each step's specific failure needing a distinct, clear error message.
async function checkout(cart, payment) {
try {
await validateCart(cart);
} catch (err) {
throw new Error('Cart validation failed: ' + err.message);
}
try {
await chargePayment(payment);
} catch (err) {
throw new Error('Payment failed: ' + err.message);
}
return createOrder(cart, payment);
}