skip to content

When a .catch() handler on a promise chain returns a value normally instead of rethrowing, what state is the promise it returns in, and how does the rest of the chain behave?

level: middleimportance: must knowfreq 62%

answer

  1. a handler transforms, not terminates
  2. returning means the failure is handled
  3. logging alone returns undefined
  4. fulfilled with the handler's return value
  5. rethrow to keep the chain failed

basics

~20 s

A .catch handler that returns normally fulfills the promise .catch returned, with that return value. The chain is back on the success path: the next .then runs with whatever the handler returned, which is undefined if it returned nothing.

solid answer

~40 s

`.catch` is a handler, not a terminator. Returning normally from it means "I handled this", so the promise `.catch` produced is *fulfilled* with the returned value and every downstream `.then` runs as if nothing failed. That is deliberate — it is how you supply a fallback, like `.catch(() => [])` before code that maps over a list. The failure mode is the accidental version: a `.catch(err => console.error(err))` inserted for logging returns `undefined`, so the next step receives `undefined` and usually dies with a confusing `TypeError` far from the real cause. If you want the chain to stay failed, the handler must rethrow (`throw err`) or return a rejected promise; only then does the rejection continue downstream. So decide per handler: recover with a usable value, or rethrow — logging alone is neither.

code

javascript · 20 lines
javascript
const failing = () => Promise.reject(new Error('db down'));

// Recovery: chain resumes on the success path
failing()
  .catch(() => ({ items: [] }))
  .then((r) => console.log('recovered:', r.items.length)); // recovered: 0

// Accidental swallow: next step receives undefined
failing()
  .catch((err) => console.log('logged:', err.message))
  .then((r) => console.log(r.items.length)) // TypeError
  .catch((err) => console.log('second failure:', err.constructor.name));

// Observe and keep failing
failing()
  .catch((err) => {
    console.log('observed');
    throw err;
  })
  .catch((err) => console.log('still rejected:', err.message));

go deeper

for a junior

Remember that .catch does not end the chain: whatever the handler returns becomes the value the next .then receives, and a handler that only logs passes along undefined.

for a middle

Explain the three completions of a rejection handler — return a value (fulfill), return a promise (adopt it), throw (reject) — and show how the logging-only handler produces a TypeError one step later.

for a senior

Show the diagnosis: a stack trace pointing at a property access on undefined usually means an upstream handler recovered accidentally, and you fix it by making each handler either return a usable fallback or rethrow.

for a principal

Own the policy — where recovery is legitimate (degraded, still-correct output) versus where a failure must reach the caller, and require that any handler which swallows produces a signal so silent degradation cannot hide in a service.

## Handlers decide the next state A rejection handler is just a callback whose completion decides the state of the promise `.catch()` returned: - **returns a value** → that promise **fulfills** with the value; - **returns a promise/thenable** → it adopts that promise's eventual state (so returning a rejected one keeps the chain failed); - **throws** → that promise **rejects** with the thrown value. There is no fourth option meaning "stay broken but do not throw". Falling off the end of a handler returns `undefined`, which is a *value*, so the chain is fulfilled with `undefined`. ## Recovery: the intended use ```js loadRecommendations() .catch(() => []) // recover with an empty list .then((items) => render(items.length)); // runs with [] on failure ``` This is the promise equivalent of a `catch` block that supplies a default. The important property is that the recovery value must be *shape-compatible* with what the success path produced — here an array, so `items.length` is safe either way. A recovery that returns something the next step cannot use has not recovered anything; it has moved the crash. ## The swallowing bug The same rule produces the most common promise bug in real code: ```js fetchUser(id) .catch((err) => console.error(err)) // returns undefined! .then((user) => user.profile.name); // TypeError: user is undefined ``` The author's intent was "log it and stop". What they wrote was "log it, then continue with `undefined`". The resulting `TypeError` points at the property access, not at the failed fetch, so the stack trace blames the wrong line — and if that second failure is also swallowed somewhere, a request quietly returns garbage instead of an error. Worse, a `.catch` at the *end* of a function whose result is returned to a caller reports success upward: ```js function getUser(id) { return fetchUser(id).catch((err) => logger.warn(err)); // resolves undefined } ``` Every caller now believes `getUser` succeeded. ## Staying failed: rethrow or return a rejection ```js .catch((err) => { metrics.increment('user.fetch.failed'); throw err; // same reason continues downstream }) ``` Rethrowing the *same* error keeps the original reason and its stack intact, which is what you want for a pure observe-and-continue handler. Throwing a *new* error is a deliberate translation — turning a low-level failure into a domain-level one at a boundary — and it replaces the reason, so preserve what you need from the original when you do it. Returning `Promise.reject(err)` is equivalent to rethrowing for propagation purposes; `throw` is the more direct expression and is preferred in a synchronous handler body. ## Where this bites hardest With `async`/`await` the same rule applies but reads differently, because falling off the end of a `catch` block also produces a normally-returning function. The promise vocabulary just makes it visible: a rejection handler is a *transformation* of the failure into some next state, and the default transformation — returning `undefined` — is almost never what you meant. A useful review heuristic: **every rejection handler must end in one of three ways** — a real fallback value, a rethrow, or being the genuine end of the chain where nothing downstream consumes the result (a top-level entry point that only reports). A handler that logs and is followed by more chain is a defect. ## Ordering note Only the *first* rejection handler downstream sees the error. Once it returns normally, the chain is fulfilled, and any further `.catch` later in the same chain is skipped entirely — it will only fire if a *later* step fails. Multiple `.catch` calls in one chain therefore do not mean "multiple chances to handle the same error"; they mean "different guards for different segments". ## What to say in an interview "Returning from `.catch` fulfills the promise it returned, so the chain resumes on the success path with that value — that is how you supply a fallback. A handler that only logs returns `undefined`, so the next step gets `undefined` and usually throws a `TypeError` far from the real cause. If the chain should stay failed, rethrow the error or return a rejected promise."

  • Is there any difference between rethrowing the caught error and returning Promise.reject(err) from the handler?
    Not for propagation — both reject the promise the handler produced with the same reason, and downstream handlers cannot tell them apart. `throw err` is preferred in a synchronous handler body because it is shorter and keeps the original error object untouched; `return Promise.reject(err)` is mainly useful when you are already returning a promise expression and want the rejected branch to read symmetrically.
  • If a chain has two .catch calls and the first one recovers, when does the second one run?
    Only if something *after* the first handler fails. Once the first handler returns normally, its promise is fulfilled and the original rejection is fully consumed, so the second `.catch` is transparent to it. Multiple catches are segment guards, not retries at handling the same error — a common misreading when people add a trailing `.catch` "just in case".
  • How would you review a .catch handler quickly for this defect?
    Ask what it returns and what comes after it. It must end in a fallback value the next step can actually use, a rethrow, or nothing downstream at all because it is the chain's endpoint. A handler that only logs or only records a metric, with more chain after it, is silently converting a failure into `undefined` and should rethrow.

saying these in an interview costs you the question

  • Thinks .catch stops the chain from continuing
  • Assumes downstream .then callbacks are skipped after a catch
  • Logs the error and expects the failure to propagate
  • Believes a later .catch will still see the handled error
  • Calls a handler returning undefined a recovery

context