Promise.prototype.catch()

Exactly then(undefined, onRejected), with one consequence people miss: a handler that merely logs turns a failure into a success, and the rest of the chain carries on with undefined.

Promise methodES2015Live demo
Common call
p.catch(e => { log(e); throw e; })
Returns
a new promise — FULFILLED if the handler returns
Replaces
the second argument to then
Watch out
returning from the handler swallows the error
promise.catch(onRejectedonRejected — Called with the rejection reason. Returning a value fulfils the chain; throwing, or returning a rejected promise, keeps it rejected.type: Function · required)
→ Promise

Demo

Live evaluation
Try:
Inputs
msgstringerror message
rethrownumber0 = return, 1 = rethrow
Output
await Promise.reject(new Error('boom')).catch(e => { if (0) throw e; return 'recovered: ' + e.message; })
Pending…

The first case is the behaviour behind most swallowed errors: because the handler returns, the chain is FULFILLED with that value, and everything downstream carries on as though nothing failed. A handler that only logs does exactly the same, returning undefined. The second case rethrows, so the promise stays rejected and the error keeps propagating — which is what a logging handler almost always should do.

Parameters

NameTypeRequiredDescription
onRejectedFunctionyesCalled with the rejection reason. Returning a value fulfils the chain; throwing, or returning a rejected promise, keeps it rejected.

Return value

Promise — A new Promise. If the handler returns normally the new promise is FULFILLED with that value — the chain recovers. Rethrow to keep it rejected.

Common patterns

Log and rethrow
Report without swallowing.
p.catch(e => { report(e); throw e; });
Recover with a fallback
Deliberate recovery — returning IS the point here.
const data = await fetchFresh().catch(() => cached);
Catch at the end of the chain
One handler covers every preceding link.
fetch(u).then(r => r.json()).then(use).catch(handle);

Examples

1. Handling resolves the chain
await Promise.reject(new Error("x")).catch(() => "recovered")
Returns
'recovered'
2. Rethrowing keeps it rejected
Promise.reject(new Error("x")).catch(e => { throw e; })
Returns
still rejected
3. It catches earlier then errors
await Promise.resolve(1).then(() => { throw new Error("in then"); }).catch(e => e.message)
Returns
'in then'
4. A bare log returns undefined
await Promise.reject(new Error("x")).catch(e => { console.log(e); })
Returns
undefined — and FULFILLED
5. It is then with one argument
Promise.prototype.catch.length
Returns
1
6. Nothing to catch is a no-op
await Promise.resolve(1).catch(() => "never")
Returns
1

Pitfalls

1. A logging handler swallows the error
The single most common promise mistake. catch(e => console.error(e)) returns undefined, which FULFILS the chain — so downstream code runs as though everything worked and receives undefined. The failure is reported and then ignored.
Chain recovers
await p.catch(e => console.error(e))
undefined, and fulfilled
Rethrow
await p.catch(e => { console.error(e); throw e; })
still rejects
2. Position in the chain decides what it covers
A catch only sees rejections from links BEFORE it. Placing it in the middle lets later errors escape entirely, which is how an unhandled rejection appears in code that visibly contains a catch.
Later errors escape
p.catch(handle).then(() => { throw new Error("late"); })
unhandled rejection
Catch last
p.then(() => { throw new Error("late"); }).catch(handle)
handled
3. It catches everything, including programming errors
A TypeError from a typo inside a then callback arrives here exactly like a network failure. A broad handler that reports "request failed" mislabels genuine bugs and makes them much harder to find. Narrow the handling by inspecting the error.
Mislabels bugs
p.catch(() => show("Network error"))
shown for a TypeError too
Discriminate
p.catch(e => {
  if (e instanceof TypeError) throw e;
  show("Network error");
})
bugs still surface
4. It does not help an unawaited promise
Attaching a catch prevents the unhandled-rejection warning, but if nothing awaits the chain the surrounding function returns before any of it runs. The error is handled and the caller has already moved on.
Not awaited
function save() { doSave().catch(log); }
returns before saving
Return or await
async function save() { await doSave().catch(log); }
the caller can wait

When to use

Use it
  • Recovering with a fallback value, deliberately
  • Logging and rethrowing at a boundary
  • A final handler at the end of a chain
  • Converting a rejection into a result your caller expects
Reach for something else
  • try/catch reads better in async functions → use that
  • You only want cleanup → finally
  • The handler only logs → rethrow, or you have silently recovered
  • Mid-chain placement → later links escape it

Notes

Complexity
O(1) to attach; the handler runs as a microtask
Return
A new Promise, fulfilled if the handler returns normally
CPython impl
V8: Builtins-promise-catch
Memory
Allocates a new promise
Thread-safe
Single-threaded

FAQ

Because the catch handler returned. Returning from a rejection handler is how you RECOVER — it fulfils the chain with that return value. A handler that only logs returns undefined, so the chain succeeds with undefined. Rethrow if you meant to propagate.

p.catch(e => { log(e); throw e; });

History

ES2015
catch added with the Promise constructor as sugar over then.
ES2017
async/await made try/catch the idiomatic form for most error handling.