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?
answer
- a handler transforms, not terminates
- returning means the failure is handled
- logging alone returns undefined
- fulfilled with the handler's return value
- rethrow to keep the chain failed
basics
~20 sA .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 linesconst 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
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.
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.
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.
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