Promises & Chaining

Medium100% Free~20 mins#promises#promises-aplus#microtasks#error-handling#finally#chaining#asynchronous
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 = [];
2
3Promise.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
Interactive Challenge

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.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
results:[]
Console Stream
> [empty]

Deep Technical Breakdown

Promises/A+ Specification & Microtask Lifecycle

  1. 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 with x.
    • If the handler throws an error e, the returned promise rejects with e.
    • If the handler returns a Promise P, the returned promise adopts the state of P.
  2. Error Recovery with .catch(): An unhandled error bubbles down the chain, skipping all .then(onFulfilled) callbacks until an onRejected handler (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.
  3. 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()`?