Error Handling in Promises
.catch, Unhandled Rejections, and Global Nets
LinkedIn Hook
In Node.js 15 and later, a single unhandled promise rejection crashes your entire process. Default behavior. No warning.
One
await fetch(url)without a.catch()somewhere up the chain, one 500 from a third-party API, and your server is gone. Your process manager restarts it. It happens again. And again. This is how "the site keeps crashing for no reason" tickets get written.The fix is a handful of patterns every async-native JS engineer needs to know cold: where
.catch()goes in a chain (position changes meaning), when to usethrowvsreturn Promise.reject(), howPromise.allfails fast vsPromise.allSettlednever failing, the Go-style "errors as values" pattern, and global nets for the ones that slip through —unhandledrejectionin the browser,process.on("unhandledRejection")in Node.In this lesson you will learn every one of them with runnable code.
Read the full lesson -> [link]
#JavaScript #InterviewPrep #Promises #AsyncAwait #NodeJS #ErrorHandling #CodingInterview #WebDevelopment
What You'll Learn
- How
.catch()position changes behavior in a chain, and the difference betweenthrowandPromise.reject() - Using
try/catchwithasync/await,Promise.allvsPromise.allSettled, and the Go-style "errors as values" pattern - Global safety nets:
window.onerror,addEventListener("error"),unhandledrejection, andprocess.on("unhandledRejection")
The Food Delivery Analogy
Think of promises like ordering food delivery. The .then() is what happens when the food arrives. The .catch() is your plan when the restaurant cancels. But here's the dangerous part: if you NEVER set up a plan for cancellation, your app just... silently starves. That's an unhandled rejection — the most common source of production crashes in Node.js.
.catch Placement Matters
The position of .catch() in a promise chain fundamentally changes behavior:
// Scenario 1: .catch at the END — catches errors from ALL previous .then()s
Promise.resolve("start")
.then((val) => {
throw new Error("Error in step 1");
})
.then((val) => {
console.log("Step 2:", val); // SKIPPED
})
.then((val) => {
console.log("Step 3:", val); // SKIPPED
})
.catch((err) => {
console.log("Caught:", err.message); // "Caught: Error in step 1"
});
// Scenario 2: .catch in the MIDDLE — catches errors ABOVE, chain continues BELOW
Promise.resolve("start")
.then((val) => {
throw new Error("Error in step 1");
})
.catch((err) => {
console.log("Caught:", err.message); // "Caught: Error in step 1"
return "recovered"; // Recovery value passed to next .then
})
.then((val) => {
console.log("Step 2:", val); // "Step 2: recovered" — chain continues!
});
// Scenario 3: Multiple .catch blocks for different sections
fetchUser()
.then(processUser)
.catch((err) => {
// Catches fetchUser or processUser errors
console.log("User error:", err.message);
return getDefaultUser(); // Recovery
})
.then(sendEmail)
.then(logResult)
.catch((err) => {
// Catches sendEmail or logResult errors
console.log("Email error:", err.message);
});
The catch-then-catch Chain
// Error in catch itself
Promise.reject("initial error")
.catch((err) => {
console.log("First catch:", err);
throw new Error("error in catch!"); // Throwing INSIDE catch
})
.catch((err) => {
console.log("Second catch:", err.message); // "error in catch!"
// A .catch can catch errors from a previous .catch!
});
Promise.reject vs throw
Inside a .then(), both throw and return Promise.reject() trigger the next .catch(). But there's a subtle difference:
// Both work the same inside .then():
Promise.resolve()
.then(() => {
throw new Error("thrown"); // Works
})
.catch((e) => console.log(e.message)); // "thrown"
Promise.resolve()
.then(() => {
return Promise.reject(new Error("rejected")); // Also works
})
.catch((e) => console.log(e.message)); // "rejected"
// BUT — outside of .then(), only Promise.reject works as expected:
function validate(input) {
if (!input) {
// throw new Error("invalid"); // This throws synchronously!
return Promise.reject(new Error("invalid")); // Returns rejected promise
}
return Promise.resolve(input);
}
// With Promise.reject, the caller can always use .catch():
validate(null).catch((e) => console.log(e.message)); // "invalid"
async/await with try/catch
async/await lets you use regular try/catch for promise errors:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof TypeError) {
// Network error — fetch itself failed
console.error("Network error:", error.message);
} else {
// HTTP error or JSON parse error
console.error("API error:", error.message);
}
throw error; // Re-throw for caller to handle
}
}
The "Error as Values" Pattern (Go-Style)
A popular pattern inspired by Go's error handling, avoiding try/catch entirely:
// Utility function
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
// Usage — clean, no try/catch nesting
async function createUser(userData) {
const [validationErr, validated] = await to(validateInput(userData));
if (validationErr) {
return { error: "Invalid input: " + validationErr.message };
}
const [dbErr, user] = await to(database.create(validated));
if (dbErr) {
return { error: "Database failed: " + dbErr.message };
}
const [emailErr] = await to(sendWelcomeEmail(user.email));
if (emailErr) {
console.warn("Email failed, but user created"); // Non-critical
}
return { data: user };
}
This pattern makes each error check explicit and avoids deeply nested try/catch blocks. The await-to-js npm package implements this exact pattern.
Error Handling in Promise.all
// Promise.all — FAST FAILS on first rejection
async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/posts").then((r) => r.json()),
fetch("/api/comments").then((r) => r.json()),
]);
return { users, posts, comments };
} catch (error) {
// Only the FIRST rejection is caught
// Other promises still run but their results are discarded
console.error("One request failed:", error.message);
}
}
// Promise.allSettled — NEVER rejects, gives all results
async function fetchAllDataSafe() {
const results = await Promise.allSettled([
fetch("/api/users").then((r) => r.json()),
fetch("/api/posts").then((r) => r.json()),
fetch("/api/comments").then((r) => r.json()),
]);
const data = {};
const errors = [];
results.forEach((result, index) => {
const keys = ["users", "posts", "comments"];
if (result.status === "fulfilled") {
data[keys[index]] = result.value;
} else {
errors.push({ key: keys[index], reason: result.reason });
}
});
return { data, errors };
}
Unhandled Rejection Events
Unhandled rejections are promises that reject without a .catch(). In Node.js 15+, they crash the process by default.
// DANGER — unhandled rejection
async function fetchData() {
const res = await fetch("/api/data"); // If this rejects...
return res.json();
}
fetchData(); // No .catch(), no try/catch — UNHANDLED REJECTION
// FIX — always handle rejections
fetchData().catch((err) => console.error("Failed:", err));
Global Error Handlers
// Browser — window.onerror (synchronous errors)
window.onerror = function (message, source, lineno, colno, error) {
console.log("Global error:", message);
console.log("Source:", source, "Line:", lineno);
// Return true to prevent default browser error logging
return true;
};
// Browser — addEventListener("error") — more capable
window.addEventListener("error", (event) => {
console.log("Error event:", event.error);
// Can capture errors that window.onerror misses (like resource load failures)
// event.preventDefault() to suppress default logging
});
// Browser — unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
console.log("Unhandled rejection:", event.reason);
event.preventDefault(); // Prevent default console error
// Send to error tracking service (Sentry, Datadog, etc.)
});
// Node.js — unhandled rejection (will crash in Node 15+)
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise);
console.error("Reason:", reason);
// Log the error, then exit gracefully
process.exit(1);
});
// Node.js — uncaught exception
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
// MUST exit — state may be corrupted
process.exit(1);
});
window.onerror vs addEventListener("error") vs unhandledrejection
| Feature | window.onerror | addEventListener("error") | unhandledrejection |
|---|---|---|---|
| Catches sync errors | Yes | Yes | No |
| Catches promise rejections | No | No | Yes |
| Catches resource load errors | No | Yes (with capture) | No |
| Access to Error object | Yes (5th param) | Yes (event.error) | N/A (event.reason) |
| Can prevent default | Return true | preventDefault() | preventDefault() |
| Multiple handlers | No (overwritten) | Yes (additive) | Yes (additive) |
Common Mistakes
- Forgetting a
.catch()at the end of a chain started from a fire-and-forget call — in Node.js 15+ the process crashes on that single unhandled rejection. - Using
throw new Error(...)at the top of anasyncfunction's sibling (non-async) caller and assuming.catch()will see it — outside a.then()or anasyncfunction,throwis synchronous and escapes the promise world entirely. ReturnPromise.reject(...)from a non-async function instead. - Reaching for
Promise.allwhen you actually need all results — one rejection discards the other fulfilled values. UsePromise.allSettledwhenever partial failure is acceptable.
Interview Questions
Q: Does .catch() position in a promise chain matter?
Yes, critically.
.catch()only handles rejections from promises ABOVE it in the chain. After.catch()returns a value, the chain continues with.then()handlers below it. This means you can create recovery points in a chain by placing.catch()in the middle.
Q: What's the difference between throw and Promise.reject() inside a .then()?
Inside
.then(), they behave identically — both cause the returned promise to reject. The difference matters OUTSIDE.then():throwis synchronous and will crash if not wrapped intry/catch, whilereturn Promise.reject()returns a rejected promise that can be caught with.catch().
Q: What is an unhandled rejection and why is it dangerous?
An unhandled rejection is a promise that rejects without any
.catch()handler. In Node.js 15+, it crashes the process by default. In browsers, it logs an error to the console. You should always handle rejections and use theunhandledrejectionevent as a safety net.
Q: How do you handle errors in Promise.all()?
Promise.all()fails fast — it rejects as soon as any single promise rejects, and you only get that one error. UsePromise.allSettled()when you want all results regardless of individual failures. Each result has{status: "fulfilled", value}or{status: "rejected", reason}.
Q: What's the difference between Promise.all and Promise.allSettled error handling?
Promise.allshort-circuits: the moment any input promise rejects, the aggregate rejects and you only see the first error; the rest of the results are unreachable.Promise.allSettlednever rejects — it resolves to an array of{status, value|reason}tuples so you can handle each success and failure independently.
Q: Explain the "error as values" pattern.
Inspired by Go's
err, data := function()pattern, you wrap async calls in a utility function that returns[error, data]tuples instead of throwing. This avoids try/catch nesting and makes each error check explicit. Theawait-to-jsnpm package implements this pattern.
Q: How do you set up a global error handler in the browser?
Use
window.addEventListener("error", ...)for synchronous errors (including resource load failures when you passtruefor capture), andwindow.addEventListener("unhandledrejection", ...)for promise rejections that were never handled. Callevent.preventDefault()inside the handler to suppress the default console log, and forward the error to your tracking service (Sentry, Datadog, etc.).
Q: What's the difference between window.onerror and window.addEventListener("error")?
window.onerroris a single-slot property — assigning a new handler overwrites the previous one, and it does not capture resource-load errors.addEventListener("error", ...)is additive (multiple listeners coexist) and, when registered with the capture phase, catches resource load failures like broken<img>or<script>URLs thatwindow.onerrormisses.
Quick Reference — Cheat Sheet
PROMISE ERROR HANDLING — QUICK MAP
.catch() position:
at END -> catches all errors above
in MIDDLE -> catches above; chain continues below with recovery value
after .catch -> can chain another .catch (catches errors thrown in the first)
throw vs Promise.reject():
inside .then() / async fn -> behave identically
outside .then() -> throw is SYNC; use `return Promise.reject(e)`
Aggregation:
Promise.all -> FAILS FAST on first rejection (other results lost)
Promise.allSettled -> NEVER rejects; [{status, value|reason}, ...]
async / await:
wrap await in try/catch; re-throw for caller if you can't handle it
Go-style: `const [err, data] = await to(promise)` (await-to-js)
Global safety nets:
Browser window.onerror -> sync errors (single handler)
Browser addEventListener("error") -> sync + resource loads (additive)
Browser addEventListener("unhandledrejection") -> async rejections
Node.js process.on("unhandledRejection") -> crashes process in 15+
Node.js process.on("uncaughtException") -> corrupted state, MUST exit
Quick-Fire (cross-lesson):
1. What's the difference between Promise.all and Promise.allSettled error handling?
2. What is an unhandled rejection?
3. How do you set up a global error handler in the browser?
4. What is the "error as values" pattern?
5. Difference: window.onerror vs window.addEventListener("error")?
Previous: Error Types Next: DOM Manipulation
This is Lesson 10.4 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.