catch()
ES6+Attaches a rejection handler callback to the promise.
Syntax
promise.catch(onRejected)Parameters
onRejected Function A function called when the Promise is rejected
Return Value
A new Promise
Examples
Promise.reject(new Error('실패!'))
.catch(error => {
console.log(error.message);
}); 📌 When to Use
Use catch() to give a promise chain a controlled failure path. Its most common home is the very end of a chain, where one handler deals with anything that went wrong in any earlier step — a failed network request, a JSON parse error, or an exception thrown inside one of your own then() callbacks. Reach for it when you can genuinely do something about the failure: substitute cached or default data so the UI still renders, translate a low-level error into a domain-specific one before rethrowing, or record the failure to a logging service. catch() is also the recovery mechanism of promise chains: because it returns a new promise, returning a normal value from the handler switches the chain back onto the success track, which is exactly what you want for optional resources. Place a mid-chain catch() when one specific step has a sensible fallback but later steps should continue, and a final catch() as a safety net at application boundaries — route handlers, event listeners, and top-level entry points — where an escaped rejection would otherwise become an unhandled error. In async/await code the same role is played by try/catch, but library code and legacy codebases make fluency with catch() unavoidable.
⚠️ Common Mistakes
Placing catch() in the middle of a chain and forgetting that recovery is the default: unless the handler rethrows, the chain continues on the success path, and later then() callbacks receive whatever the catch() returned — often undefined. Steps positioned after the catch may then run against missing data and fail in stranger ways than the original error.
Swallowing errors with an empty handler like catch(() => {}). The rejection disappears, no log entry is written, and the application silently misbehaves; these are among the hardest production bugs to trace. At minimum, log the error with enough context to identify the failing operation.
Forgetting catch() entirely. An unhandled rejection fires the window unhandledrejection event in browsers, and in modern Node.js it crashes the process with a non-zero exit code — a missing catch on a background refresh task can take down an entire server.
Expecting catch() to intercept errors thrown before the promise exists. If the synchronous code that creates the promise throws — for example, invalid arguments passed to a builder function — the exception propagates immediately and never reaches the chain. Only failures that occur inside the promise machinery are routed to catch().
Rejecting or rethrowing with plain strings. throw 'failed' gives the handler no stack trace, no error name, and nothing for instanceof checks. Always rethrow Error instances, wrapping the original error as the cause when you translate failures between layers.
✅ Best Practices
Use catch() deliberately for recovery: returning a value from the handler switches the chain back to the fulfilled state. Make the fallback explicit and well-shaped — a default object or an empty list — so downstream code can rely on its structure without null checks everywhere.
When you cannot fully handle a failure, log it and rethrow: catch(e => { report(e); throw e; }). This preserves the rejection for callers who own the user experience while guaranteeing the event is recorded exactly once.
Discriminate before you handle. Check error.name, use instanceof, or inspect a status property, and rethrow anything you did not anticipate — a catch() that treats a programming bug the same as a network timeout hides real defects behind retry logic.
Put the final catch() at the outermost boundary that owns the user experience — the click handler, route loader, or job runner — rather than deep inside utility functions, so policy decisions about retries and error messages live in one predictable place.
Pair catch() with finally() when cleanup must run on both the success and failure paths; keeping cleanup out of the catch handler prevents it from being skipped when the chain succeeds.
⚡ Performance Notes
catch(fn) is literally sugar for then(undefined, fn), so it costs the same: one extra promise allocation and one microtask hop when the handler runs. The measurable expense of the failure path comes from the errors themselves — constructing an Error captures a stack trace, which is orders of magnitude slower than creating an ordinary object. That is negligible for genuinely exceptional failures, but it becomes real overhead if you use rejections for routine control flow, such as signalling 'not found' on every cache lookup inside a hot loop; prefer sentinel return values there. Attaching a catch() does not deoptimize anything, unlike old try/catch folklore, so add handlers freely. One subtlety worth knowing: unhandled-rejection detection runs when the microtask queue drains, so attaching catch() asynchronously — seconds after creating the promise — can still trigger unhandled-rejection reporting in some environments even though the error eventually gets handled.
🌍 Real World Example
Resilient API Client
A resilient profile loader that never breaks the page: it validates the HTTP status, parses the body, and — only if something failed — logs the error and substitutes a clearly marked fallback user object. Because the catch() returns a value, callers always receive a usable result and can check the error flag to render a degraded state. This pattern suits non-critical widgets like avatars and recommendation panels, where showing defaults beats showing an error screen; for critical flows such as checkout, rethrow instead so the failure stays visible.
function fetchUserProfile(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Failed to fetch user:', error.message);
// Return fallback data
return {
id: userId,
name: 'Unknown User',
avatar: '/default-avatar.png',
error: true
};
});
}
// Usage - always returns data, even on failure
fetchUserProfile(999).then(user => {
console.log(user.name); // "Unknown User" if failed
});