
When a promise gets rejected, it essentially signals that an operation failed or encountered an error. Unlike synchronous code where exceptions propagate immediately, promise rejections are asynchronous events that you can catch later using .catch() or the second argument to .then(). This separation is subtle but critical for effective error handling.
It is important to realize that a rejected promise doesn’t throw an error in the traditional sense right away. Instead, it returns a promise in a rejected state. If you don’t handle the rejection, JavaScript engines will eventually warn about an “unhandled promise rejection,” which can sometimes crash the process or lead to unexpected behavior.
Consider this snippet:
let p = new Promise((resolve, reject) => {
reject(new Error("Something went wrong"));
});
p.then(result => {
console.log("Success:", result);
});
Here, p is rejected, but since there’s no rejection handler, the error is effectively lost. The promise stays rejected, and unless you add a .catch() or a second then callback for rejection, you won’t handle the error properly.
The mechanics behind this involve the microtask queue. When a promise rejects, any attached rejection handlers are queued as microtasks to be executed after the current synchronous execution completes. This means you can attach handlers even after the rejection occurs, and they’ll still be called.
For example:
let p = Promise.reject("Failed");
setTimeout(() => {
p.catch(err => {
console.log("Caught error:", err);
});
}, 100);
Despite attaching the catch after 100 milliseconds, the rejection handler still fires. This behavior contrasts with synchronous exceptions, which can’t be caught once they’ve propagated past a certain point.
Another nuance is that if a promise rejection is handled and then you throw inside the catch, the promise chain stays rejected. Consider:
Promise.reject("Initial error")
.catch(err => {
console.error("Handling:", err);
throw new Error("New error");
})
.catch(err => {
console.error("Caught new error:", err.message);
});
This pattern allows you to transform errors or propagate them along the chain, but you must be mindful that the chain remains in a rejected state until handled.
Lastly, unhandled rejections have different behavior in Node.js vs browsers. Node.js emits a process-level warning or even crashes depending on the version and flags, while browsers typically log warnings to the console. This means you can’t ignore unhandled rejections if you want robust applications.
To recap, promise rejection mechanics hinge on asynchronous error signaling, microtask queuing of handlers, and the ability to attach handlers after rejection. Understanding these details clarifies why sometimes errors seem to “disappear” or why late handlers still catch rejections. The next step is using this understanding to structure your error handling in promises effectively—
Apple 2026 MacBook Air 13-inch Laptop with M5 chip: Built for AI, 13.6-inch Liquid Retina Display, 16GB Unified Memory, 512GB SSD, 12MP Center Stage Camera, Touch ID, Wi-Fi 7; Midnight
$1,071.00 (as of June 2, 2026 22:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Best practices for error handling in promises
One fundamental best practice is to always attach a rejection handler to every promise you create. This can be done by appending a .catch() at the end of the chain or by providing a second callback to .then(). Omitting this leaves your code vulnerable to unhandled rejections. For instance:
fetch("/api/data")
.then(response => response.json())
.then(data => {
console.log("Data received:", data);
})
.catch(error => {
console.error("Fetch error:", error);
});
By including a catch handler, you ensure that any error, whether from network failure or a thrown exception in the chain, is caught and can be managed gracefully.
Another recommendation is to avoid mixing rejection handlers with synchronous try/catch blocks expecting to catch asynchronous errors. Since promise rejections happen asynchronously, a try/catch won’t intercept them:
try {
Promise.reject("Error");
} catch (e) {
// This block won't run because rejection is async
console.error("Caught:", e);
}
Instead, rely solely on promise methods to handle errors.
When chaining multiple promises, it’s often better to place a single catch at the end of the chain rather than multiple intermediate handlers. This centralizes error handling and avoids swallowing errors unintentionally:
doStep1()
.then(result1 => doStep2(result1))
.then(result2 => doStep3(result2))
.catch(err => {
console.error("Error in any step:", err);
});
If you need to handle specific errors differently at intermediate stages, you can catch and rethrow to propagate or transform errors:
doStep1()
.catch(err => {
if (isRecoverable(err)) {
return recoverFromError(err);
}
throw err; // Propagate non-recoverable errors
})
.then(result => doStep2(result))
.catch(err => {
console.error("Unrecoverable error:", err);
});
It’s also advisable to avoid empty or overly generic catch blocks that suppress errors silently. Doing so can make debugging difficult and hide critical failures:
promise
.catch(() => {
// Empty catch: error swallowed, no trace
});
Instead, always log or handle the error meaningfully.
When working with async/await syntax, which is syntactic sugar over promises, use try/catch blocks around the awaited calls to catch rejections:
async function fetchData() {
try {
const response = await fetch("/api/data");
const data = await response.json();
console.log("Data:", data);
} catch (error) {
console.error("Async error:", error);
}
}
This pattern integrates promise rejection handling seamlessly into familiar synchronous-style code.
Finally, consider adding global handlers for unhandled promise rejections in Node.js and browsers to catch any missed errors. In Node.js, you can listen to the unhandledRejection event:
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled rejection at:", promise, "reason:", reason);
// Optionally exit process or perform cleanup
});
In browsers, you can listen for unhandledrejection events on the window:
window.addEventListener("unhandledrejection", event => {
console.error("Unhandled rejection:", event.reason);
});
These handlers act as a last line of defense and can aid in diagnosing issues that slip through promise chains.
