skip to content

In a promise chain, what is the difference between handling errors with the second argument of .then(onFulfilled, onRejected) and appending a separate .catch()?

level: middleimportance: should knowfreq 55%

answer

  1. same method, different position
  2. siblings, not upstream and downstream
  3. one link earlier in the chain
  4. catch is then(undefined, fn)
  5. cannot catch its own success callback

basics

~20 s

The second argument of .then only handles rejections coming from upstream; it cannot see an error thrown by the onFulfilled callback sitting beside it. An appended .catch runs after that callback, so it covers upstream failures and the callback's own failures.

solid answer

~40 s

Both are the same mechanism — `.catch(fn)` is specified as `.then(undefined, fn)` — so the real difference is *position* in the chain. In `.then(onFulfilled, onRejected)`, the two handlers are alternatives for the same upstream promise: exactly one of them runs, and if `onFulfilled` throws, `onRejected` is not called, because it was never a downstream handler. Writing `.then(onFulfilled).catch(onRejected)` instead puts the rejection handler one link further along, where it catches both the upstream rejection and anything `onFulfilled` throws. So `.catch` is the sane default. The two-argument form is the right tool when you deliberately want to handle an upstream failure *without* swallowing errors from your own success path — for example recovering from a failed fetch while letting a rendering bug propagate.

code

javascript · 11 lines
javascript
const boom = () => {
  throw new Error('render failed');
};

Promise.resolve('ok').then(boom, () => console.log('second arg: not called'));
// (rejection escapes this chain unhandled)

Promise.resolve('ok')
  .then(boom)
  .catch((err) => console.log('trailing catch:', err.message));
// trailing catch: render failed

go deeper

for a junior

Know that a .catch at the end of the chain is the safe default, and that the second argument of .then only reacts to failures from earlier steps.

for a middle

Explain that .catch(fn) is .then(undefined, fn) and that the difference is purely positional — sibling handlers are alternatives for one settlement, a downstream handler also sees the success callback's throw.

for a senior

Demonstrate deliberate scoping: use the two-argument form when a per-step recovery must not mask bugs in the consumer, and back it with a trailing .catch so nothing escapes the chain unhandled.

for a principal

Own the convention across a codebase — decide whether per-step recovery is allowed at all, and require that any handler which swallows a failure records it, so error-handling shape does not silently vary team to team.

## They are the same primitive `Promise.prototype.catch(fn)` is defined as calling `this.then(undefined, fn)`. There is no extra machinery. So the question is never "which method handles errors better" — it is "where in the chain does the rejection handler sit?" ## Two handlers on the same link vs. one on the next link Consider the two shapes: ```js p.then(onFulfilled, onRejected); // A: alternatives for the same promise p.then(onFulfilled).catch(onRejected); // B: handler on the derived promise ``` In shape **A**, `p` settles once, and its outcome selects exactly one of the two callbacks. If `p` fulfills, `onFulfilled` runs and `onRejected` is discarded — permanently, including if `onFulfilled` itself throws. That throw rejects the promise returned by `.then`, and the rejection travels *downstream* looking for a handler; `onRejected` is not downstream, it is a sibling. In shape **B**, `.then(onFulfilled)` produces a new promise, and `.catch(onRejected)` handles *that* one. It therefore sees two kinds of failure: a rejection of `p` passed through the fulfillment-only `.then`, and a rejection produced by `onFulfilled` throwing. ```js Promise.resolve('ok') .then( () => { throw new Error('render failed'); }, () => console.log('A: not called'), ) .catch((err) => console.log('B: caught', err.message)); // B: caught render failed ``` ## Why the difference is useful, not just a trap The two-argument form gives you something `.catch` cannot: **scoped** error handling. Its handler covers exactly the upstream step and nothing else. That is what you want when a specific step has a specific recovery, and you do not want that recovery to accidentally absorb unrelated bugs in the code that consumes the result: ```js loadSettings() .then( (settings) => applySettings(settings), () => applySettings(DEFAULTS), // only a load failure lands here ) .catch(reportCrash); // applySettings bugs land here ``` A single trailing `.catch` cannot express that: it cannot tell whether the failure came from `loadSettings` or from `applySettings`. The two-argument form draws the boundary structurally. The mirror-image trap is real too. A `.catch` placed *before* the rest of the chain only guards what precedes it: ```js fetchUser() .catch(() => null) // guards fetchUser only .then((u) => u.name); // TypeError here is NOT handled ``` So "use `.catch`" is shorthand for "put the rejection handler after everything you want it to cover", not for a different feature. ## Pass-through rules that make it work Two spec details explain the routing: - If the fulfillment handler is missing or not a function, a fulfilled value passes through unchanged. That is what lets `.catch` sit in the middle of a chain of successful steps without disturbing them. - If the rejection handler is missing or not a function, a rejection passes through unchanged. That is what lets a rejection skip every plain `.then(fn)` on its way to the nearest handler. Because `.catch` supplies only the second handler, it is transparent to success and opaque to failure — which is exactly the behaviour you want at the end of a pipeline. ## Practical guidance - Default to a trailing `.catch` for the whole chain; it is the shape most readers expect, and it covers every step. - Reach for `.then(onFulfilled, onRejected)` only when the narrower scope is the point — a per-step fallback whose recovery must not mask errors in the success path. - Never write `.then(onFulfilled, onRejected)` believing it is a defensive wrapper around `onFulfilled`. It is the one thing it is not. - Both forms can appear in one chain: a scoped two-argument handler for a known recoverable step, plus a trailing `.catch` as the backstop for everything else. ## What to say in an interview "`.catch(fn)` *is* `.then(undefined, fn)`, so the only difference is where the handler sits. The second argument of `.then` is an alternative to its own success callback and can never catch that callback's throw; a trailing `.catch` sits one link downstream and catches both. I use the two-argument form when I deliberately want a per-step recovery that does not swallow bugs in the consumer."

  • Given they are the same primitive, when would you deliberately choose the two-argument form?
    When a step has its own fallback and you must not let that fallback absorb later bugs. `load().then(apply, () => apply(DEFAULTS)).catch(report)` recovers only from a load failure; anything `apply` throws skips the fallback and reaches `report`. A single trailing `.catch` cannot distinguish those two sources, so the two-argument form encodes the boundary structurally.
  • What is wrong with putting .catch in the middle of a chain rather than at the end?
    It only guards the steps above it. `fetchUser().catch(() => null).then(u => u.name)` handles the fetch failure but then hands `null` to the next step, which throws a TypeError that nothing catches. A mid-chain `.catch` is correct only when it genuinely recovers — returning a value the rest of the chain can use — and there is still a handler downstream.
  • If both a second-argument handler and a trailing .catch are present and the upstream promise rejects, do both run?
    No. The second-argument handler consumes the rejection and, if it returns normally, the promise it produces is fulfilled — so the trailing `.catch` is skipped and the chain continues on the success path. The `.catch` runs only if that handler itself throws or returns a rejected promise.

saying these in an interview costs you the question

  • Says the second argument catches errors from its own success callback
  • Claims .catch is a different mechanism from .then
  • Thinks both handlers can run for one settlement
  • Puts .catch first and assumes it guards later steps
  • Believes the two-argument form is deprecated or never useful

context