Promise.prototype.catch()
catch(onRejected) The catch() method attaches a callback function that handles Promise rejection. It is essentially a shorthand for promise.then(null, onRejected) but is more readable and the preferred way to handle errors in Promise chains.
Syntax
promise.catch(onRejected)
promise.catch(reason => {
// handle error
})
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
onRejected | function | undefined | Called if the Promise is rejected. Receives the rejection reason (error) as an argument. |
Return Value
A new Promise that:
- Resolves with the return value of
onRejectedif it does not throw - Resolves with the original fulfillment value if the original Promise resolved
- Rejects with the error thrown by
onRejectedif it throws
Examples
Basic error handling
fetch("/api/user")
.then(response => response.json())
.catch(error => {
console.error("Failed to fetch user:", error.message);
});
This first example handles the simplest case: a single fetch followed by a catch that logs the failure. The catch() method runs only when the promise rejects, which happens on network failures or HTTP errors thrown from the then handler.
Chaining after multiple operations
fetch("/api/data")
.then(data => validate(data))
.then(processed => save(processed))
.then(result => console.log("Success:", result))
.catch(error => {
console.error("Operation failed:", error.message);
});
When multiple then() calls precede a single catch(), any rejection along the chain jumps straight to the error handler. The intermediate then() handlers are skipped entirely, and the catch() receives whatever rejection reason caused the jump. This flat error path is much easier to follow than nested callbacks.
Recovering from errors
fetch("/api/user")
.then(response => response.json())
.catch(error => {
// Return default user on failure
return { name: "Guest", id: 0 };
})
.then(user => {
console.log("User:", user.name); // Works even if fetch failed
});
The recovery pattern works because catch() returns a new promise. If the onRejected handler returns a value (like the fallback user object above), that value becomes the fulfilled result of the returned promise, and the chain resumes with the next then().
Catching specific errors
async function getData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
return await response.json();
} catch (error) {
if (error.message === "HTTP 404") {
return { data: [] }; // Handle missing gracefully
}
if (error.message === "HTTP 500") {
throw new Error("Server error - please try again later");
}
throw error;
}
}
When using async/await, selective error handling moves into the catch block. Each condition checks the error type or message and responds differently: a 404 gets a default, a 500 gets re-thrown, and unexpected errors propagate up. This pattern is the async equivalent of multiple catch clauses in synchronous code.
Short-circuiting a chain
Promise.resolve(10)
.then(x => {
if (x > 5) throw new Error("Too big");
return x;
})
.catch(error => {
console.log(error.message); // "Too big"
return 0; // Recover with default
})
.then(x => {
console.log(x); // 0 - chain continues!
});
Short-circuiting shows how catch() in the middle of a chain can intercept an error, handle it, and let the chain continue with a recovery value. The then() after the catch() receives 0 instead of the original 10, proving the error was caught and the stream was repaired.
Using with finally
fetch("/api/resource")
.then(response => response.json())
.catch(error => {
console.error("Error:", error.message);
return null;
})
.finally(() => {
console.log("Request completed"); // Always runs
});
Pairing catch() with finally() gives you two separate hooks: one for error handling and one for cleanup that always runs. The finally() handler doesn’t receive any arguments and doesn’t change the resolved value, so it’s purely for side effects like hiding a loading indicator.
Common Mistakes
Not Returning in catch
// WRONG: Lost error, but also breaks the chain
fetch("/api/data")
.then(data => process(data))
.catch(error => {
console.error(error.message);
})
.then(result => {
console.log(result); // undefined if error occurred!
});
// CORRECT: Return a value or re-throw
fetch("/api/data")
.then(data => process(data))
.catch(error => {
console.error(error.message);
return null; // Recover with null
})
.then(result => {
console.log(result); // null if error occurred, or actual result
});
The broken version silently drops the error value after logging it. The next then() runs with undefined because the catch() handler returned nothing. Always return a value or re-throw from inside catch() if the chain needs to keep working.
Swallowing errors
// BAD: Silently ignoring errors
fetch("/api/data").catch(() => {});
// BETTER: At least log something
fetch("/api/data").catch(err => console.warn("Error:", err));
// BEST: Handle appropriately
fetch("/api/data")
.then(data => render(data))
.catch(error => showErrorToast(error.message));
An empty catch(() => {}) hides a failure without doing anything about it. At minimum, log the error so you can debug it. The best approach depends on the context: a dashboard widget might show a toast, while a background sync might retry silently.
Placing catch before other then handlers
// WRONG: catch might catch errors from later in the chain
fetch("/api/user")
.catch(error => console.error(error))
.then(user => user.name) // This can still throw!
.then(name => console.log(name));
// CORRECT: catch at the end
fetch("/api/user")
.then(user => user.name)
.then(name => console.log(name))
.catch(error => console.error(error));
How catch() works internally
catch(onRejected) is exactly equivalent to then(undefined, onRejected). It passes undefined as the fulfillment handler, which means fulfilled values pass through unchanged. The rejection handler can either return a value (recovering from the error and resolving the returned promise), return another promise (chaining asynchronous recovery), or throw (re-rejecting with the same or a new error).
When catch() should be at the end
In a promise chain, catch() is usually easiest to reason about when it sits at the end. That way it can see both the initial rejection and any error thrown by earlier then() handlers. Placing it early can hide later failures, which makes debugging harder. A final catch() is a safety net for the whole sequence and keeps the happy path uncluttered.
Catching errors thrown in then() handlers
A catch() at the end of a chain handles any error from the preceding chain — the initial promise rejection and every error thrown by a then() handler before it. This is one of the key advantages of promise chains over raw callbacks: a single catch() at the end is a safety net for the entire preceding chain.
Promise.resolve('data')
.then(data => { throw new Error('Processing failed'); })
.then(result => console.log(result)) // skipped
.catch(err => console.error(err.message)); // 'Processing failed'
Unhandled rejections
If a promise rejects and no .catch() handler is attached before the current microtask queue drains, most runtimes emit an unhandledRejection event (Node.js) or a console warning (browsers). In Node.js 15+, unhandled rejections crash the process by default. Always attach a .catch() — or use try/catch with await — on any promise chain that might reject, even when you only want to suppress the error.
See Also
- Promise.prototype.then() — The foundation of Promise chaining
- Promise.prototype.finally() — Run code regardless of outcome
- Promise.resolve() — Create resolved Promises
- Promise.reject() — Create rejected Promises