What it does: Catches and handles errors anywhere in a promise chain using .catch() - errors “bubble up” to the nearest error handler.
Error Propagation:
Errors jump to nearest .catch():
javascript
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`)) // any of these can fail
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.log("Something broke:", error)); // catches ALL errors aboveMultiple catches:
javascript
promise
.then(data => processData(data))
.catch(error => console.log("Process failed:", error)) // catches processData errors
.then(result => saveData(result))
.catch(error => console.log("Save failed:", error)); // catches saveData errorsImplicit try/catch:
Throwing errors automatically rejects:
javascript
new Promise((resolve, reject) => {
throw new Error("Boom!"); // same as reject(new Error("Boom!"))
}).catch(error => console.log(error));
// Also in .then handlers
promise
.then(data => {
throw new Error("Processing failed"); // automatically caught
})
.catch(error => console.log(error)); // "Processing failed"Even programming errors get caught:
javascript
promise
.then(data => {
someUndefinedFunction(); // ReferenceError
})
.catch(error => console.log(error)); // catches the ReferenceError!Rethrowing Errors:
Handle some, rethrow others:
javascript
promise
.catch(error => {
if (error.name === 'ValidationError') {
console.log("Validation failed, showing user message");
return "default data"; // recover and continue
} else {
throw error; // rethrow - goes to next catch
}
})
.then(data => console.log(data)) // runs if we recovered
.catch(error => console.log("Unhandled error:", error));Interview Gotchas:
javascript
// 1. Forgetting .catch at the end
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
// If fetch fails, error goes unhandled! 💥
// 2. .catch continues the chain
promise
.then(data => { throw new Error("fail"); })
.catch(error => "recovered") // returns "recovered"
.then(data => console.log(data)); // "recovered" - chain continues!
// 3. Unhandled rejections
window.addEventListener('unhandledrejection', event => {
console.log('Unhandled promise rejection:', event.reason);
});
new Promise(() => { throw new Error("Oops!"); }); // triggers unhandledrejectionBottom line: Errors bubble up to the nearest .catch(). Throwing in any handler automatically rejects the promise. Always end chains with .catch() to handle errors gracefully.
The key difference: In regular try/catch, throwing from catch goes UP to the caller. In promises, returning from .catch goes DOWN the chain.
Regular try/catch behavior:
javascript
function doSomething() {
try {
throw new Error("initial error");
} catch (error) {
return "recovered"; // goes back to caller as normal result
}
}
try {
let result = doSomething();
console.log(result); // "recovered" - normal flow continues
} catch (error) {
console.log("Won't run");
}Promise .catch() behavior:
javascript
promise
.then(data => { throw new Error("fail"); })
.catch(error => {
// returning = resolving a new promise
return "recovered"; // creates Promise.resolve("recovered")
})
.then(data => console.log(data)); // "recovered" - gets the resolved valueThe Mental Model:
Every .then/.catch/.finally returns a NEW promise:
javascript
// What's really happening:
promise
.then(data => { throw new Error("fail"); }) // returns rejected promise
.catch(error => "recovered") // returns resolved promise with "recovered"
.then(data => console.log(data)); // gets "recovered" from resolved promise
// To continue error chain, you must THROW:
promise
.catch(error => {
if (canRecover) return "fixed"; // ✅ recovery - goes to .then
else throw error; // ❌ still broken - goes to .catch
})The Rule:
- Return from .catch = “I fixed it” → goes to next .then
- Throw from .catch = “Still broken” → goes to next .catch
This is why promises are more powerful than regular try/catch - you can recover mid-chain and continue normal flow!
