Promises & Chaining
Key Learning Objectives
Understand Promises/A+ chaining and how every .then() returns a fresh promise.
Master error propagation and how .catch() recovers rejected promises back to fulfilled state.
Learn why .finally() runs unconditionally and passes through the prior settled value.
Trace asynchronous microtask queue scheduling during multi-stage promise reactions.
The Interview Problem
What output is logged to the console when the following promise chain executes, and how do errors propagate through .catch() and .finally()?
1const results = [];23Promise.resolve('start')4 .then((val) => {5 results.push(val);6 throw new Error('fail');7 })8 .then(() => {9 results.push('unreachable');10 })11 .catch((err) => {12 results.push('caught: ' + err.message);13 return 'recovered';14 })15 .finally(() => {16 results.push('cleanup');17 return 'ignored';18 })19 .then((val) => {20 results.push('end: ' + val);21 console.log(results.join(' -> '));22 });
Predict Console Output
Select the option that matches what standard ECMAScript prints to the console:
start -> caught: fail -> cleanup -> end: recovered
start -> caught: fail -> cleanup -> end: ignored
start -> unreachable -> caught: fail -> cleanup -> end: recovered
start -> caught: fail -> end: recovered
V8 Engine Execution Trace
Step 1 of 8 (Line 1)Global script execution begins. An empty results array is allocated in heap memory.
Deep Technical Breakdown
Promises/A+ Specification & Microtask Lifecycle
- Chaining Returns New Promises: In the Promises/A+ specification, calling
.then()or.catch()does not mutate the original promise; it returns a completely new Promise whose state depends on the outcome of the handler:- If the handler returns a value
x, the returned promise resolves withx. - If the handler throws an error
e, the returned promise rejects withe. - If the handler returns a Promise
P, the returned promise adopts the state ofP.
- If the handler returns a value
- Error Recovery with .catch(): An unhandled error bubbles down the chain, skipping all
.then(onFulfilled)callbacks until anonRejectedhandler (like.catch()) is encountered. If.catch()returns normally (e.g.return 'recovered'), the returned promise transitions back to fulfilled state. Downstream.then()blocks will treat this as a successful resolution. - The .finally() Passthrough Guarantee: Introduced in ES2018,
.finally(callback)is guaranteed to run when the promise settles (either fulfilled or rejected). Key behaviors:- The callback receives zero arguments.
- Any value returned from the callback is ignored; the promise adopts the fulfillment value of the preceding promise (
'recovered'). - Exception: If the
.finally()callback itself throws an exception or returns a rejected promise, the chain rejects with that new error.
Common Traps & Mistakes
Assuming returning from `.catch()` leaves the promise in a rejected state. It recovers the promise to fulfilled unless you re-throw.
Expecting `.finally()` to transform the resolved value like `.then()` does. It cannot modify the value unless it throws.
Forgetting that `.then(onFulfilled, onRejected)` cannot catch errors thrown inside its own `onFulfilled` handler—only subsequent chained `.catch()` blocks can.
FAANG Follow-Up Probes
Probe #1
What is the difference between `.then(null, onRejected)` and `.catch(onRejected)`?
Probe #2
How does `.finally()` handle an unhandled rejection when the finally callback completes successfully?
Probe #3
How would you implement `Promise.prototype.finally` from scratch using `.then()`?
