JavaScript Promise Error Handling
Learn JavaScript promise error handling: .catch() placement, rejection propagation, rethrowing and recovering, .finally(), and try/catch with async/await.
JavaScript promises are a fundamental part of managing asynchronous operations, allowing developers to handle asynchronous events with more flexibility and ease (see JavaScript: Promises). Error handling in promises is crucial for writing robust JavaScript code that can deal with unexpected issues without crashing the application.
This chapter covers how rejections are caught, how errors flow through a chain, how to rethrow and recover, the .finally() cleanup hook, the global unhandledrejection safety net, and how the same patterns map onto async/await. If you are new to chaining .then() calls, read Promises chaining first.
A promise can end in one of two states: fulfilled (it produced a value) or rejected (something went wrong). A rejection happens when you call reject(...), when you throw inside an executor or a .then() callback, or when a built-in async API fails. Error handling is about routing those rejections to a handler instead of letting them crash your program.
Error handling in promises is accomplished using the .catch() method or by passing a second argument to the .then() method. Both methods provide ways to manage and recover from errors that occur during the execution of asynchronous operations.
Using .catch() Method
The .catch() method is used to catch any errors that occur during the execution of the promise chain.
Using Second Argument of .then()
Alternatively, a second argument can be passed to .then() to handle errors that occur after the first argument's callback executes.
Advanced Error Handling Techniques
Error Flow in Chains
Errors should be propagated correctly through the promise chain to ensure they are handled at the appropriate level. For example, if you place the .catch() block before the .then() block, the .then() block will still execute. Because .catch() resolves the promise chain (unless it re-throws an error), the subsequent .then() receives undefined as its argument.
When you place the .catch() block before the .then() block, any error thrown inside the .then() will not be caught by the preceding .catch(). It will only be handled if you add another .catch() block after it.
Rejection Propagation
You do not need a .catch() after every .then(). A rejection skips all the success handlers (the first argument of .then()) and travels down the chain until it reaches the first .catch(). This lets you write a long chain of steps and handle any failure in one place at the end.
In the example below, the rejection happens at the very start, yet none of the three .then() callbacks run — control jumps straight to the single .catch():
Rethrowing and Recovering
A .catch() handler does two jobs depending on what it does:
- Recover — if it returns a value (or returns nothing), the chain becomes fulfilled again and the next
.then()runs with that value. This is how you supply a fallback. - Rethrow — if it
throws (or returns a rejected promise), the error keeps propagating to the next.catch(). Use this when a handler can't fully deal with the error and wants a later handler to finish the job.
Handling Specific Errors
JavaScript allows for more nuanced error handling strategies, such as filtering errors based on their type or the specific circumstances of the error. In the following example we handle a TypeError. A TypeError typically happens when a value is not of the expected type and therefore our desired operation cannot be done.
Best Practices for Promise Error Handling
- Always return or throw errors in catch blocks to ensure that errors do not go silently ignored.
- Chain promises properly to ensure that errors are caught and handled.
- Use finally blocks where necessary to perform cleanup tasks, regardless of the promise’s outcome.
Implementing a Finally Block
The finally() method is used to execute a block of code after promises settle, regardless of the outcome.
Catching Unhandled Rejections
If a promise rejects and no .catch() ever handles it, the error is lost silently — there is no surrounding try/catch that synchronous code would have. To avoid bugs disappearing, the environment fires a global unhandledrejection event you can listen for. This is a last-resort safety net for logging and reporting, not a replacement for a real .catch().
In the browser:
In Node.js the equivalent is process.on('unhandledrejection', (reason) => { ... }).
Error Handling with async/await
async/await is built on promises, so the same rejections occur — but you catch them with an ordinary try/catch block, which reads like synchronous code. An await on a rejected promise throws inside the async function, and try/catch intercepts it. See JavaScript: async/await for the full picture.
A try/catch inside an async function only catches errors from promises you await. If you call a promise-returning function without await, its rejection escapes the try/catch — you must either await it or attach a .catch().
Conclusion
Effective error handling in JavaScript promises is essential for developing reliable and resilient web applications. By understanding the .catch() method, the second argument of .then(), rejection propagation, rethrowing, and the .finally() cleanup hook, developers can ensure that their applications handle asynchronous errors gracefully and recover instead of crashing. When working with async/await, wrap awaited promise calls in try/catch blocks for synchronous-looking error handling, and keep a global unhandledrejection listener as a safety net for the cases you miss.
To go deeper, continue with Promises chaining and the Promise API.