# Promise Chaining

## 1. The Mechanism: Each `.then()` Returns a New Promise

```js
Promise.resolve(1)
  .then(n => n + 1)   // returns a new Promise resolved with 2
  .then(n => n * 10)   // returns a new Promise resolved with 20
  .then(n => console.log(n)); // 20
```

Because every `.then()` returns a **new** Promise (see the Promises topic), chains can be built to any length, with each step transforming the previous result.

## 2. Returning a Promise from `.then()` — Automatic Flattening

If a `.then()` callback returns **another Promise**, the chain automatically waits for it and unwraps its value — it does not nest Promises inside Promises.

```js
function fetchUser(id) { return Promise.resolve({ id, name: "Ana" }); }
function fetchOrders(userId) { return Promise.resolve([{ total: 20 }]); }

fetchUser(1)
  .then(user => fetchOrders(user.id))  // returns a Promise — the chain WAITS for it
  .then(orders => console.log(orders)); // orders is the ARRAY, not a Promise wrapping an array
```

This flattening behavior is what makes sequential async steps composable — without it, chains would produce nested `Promise<Promise<Promise<value>>>` structures.

![A .then() callback that returns a Promise gets auto-awaited, so the next .then() receives the unwrapped value](diagrams/01-flattening.svg)

## 3. Returning a Plain Value — Automatically Wrapped

```js
Promise.resolve(1)
  .then(n => n + 1)       // returning a plain number — automatically becomes Promise.resolve(2)
  .then(n => console.log(n)); // 2
```

Every `.then()` callback's return value — Promise or plain value — is normalized the same way: the next `.then()` in the chain always receives the unwrapped, resolved value either way.

## 4. Error Propagation Skips Directly to the Next `.catch()`

```js
Promise.resolve()
  .then(() => { throw new Error("step 1 failed"); })
  .then(() => console.log("step 2 — SKIPPED"))   // skipped — error propagates past it
  .then(() => console.log("step 3 — SKIPPED"))   // skipped too
  .catch(e => console.log("caught:", e.message))  // "caught: step 1 failed"
  .then(() => console.log("step 4 — runs, chain has recovered")); // runs — .catch() "handles" the error
```

Once an error occurs, every subsequent `.then()` is skipped until a `.catch()` (or a `.then(onFulfilled, onRejected)` with a rejection handler) is reached. After that handler runs (without re-throwing), the chain is considered "recovered" and continues normally.

## 5. Placing `.catch()` Strategically

```js
// ONE catch at the end — handles errors from ANY step above it
step1().then(step2).then(step3).catch(handleAnyError);

// Per-step catch — recovers locally, chain continues with a fallback value
step1()
  .then(step2)
  .catch(err => {
    console.log("step2 failed, using fallback");
    return fallbackValue; // chain continues as if step2 had succeeded with this value
  })
  .then(step3);
```

## 6. Chaining vs Nesting — the Anti-Pattern to Avoid

```js
// BAD — nested .then() inside .then(), reintroducing "callback hell" with Promises
fetchUser(1).then(user => {
  fetchOrders(user.id).then(orders => {
    fetchOrderDetails(orders[0].id).then(details => {
      console.log(details);
    });
  });
});

// GOOD — flat chain, each step returns a Promise for the next .then() to pick up
fetchUser(1)
  .then(user => fetchOrders(user.id))
  .then(orders => fetchOrderDetails(orders[0].id))
  .then(details => console.log(details));
```

If you find yourself nesting `.then()` calls, you almost always meant to `return` the inner Promise and chain it flatly instead.

## Under the Hood: "Thenables" — Why Flattening Works on Non-Promise Objects Too

Section 2's flattening behavior is actually specified more broadly than "if you return a real Promise" — the underlying algorithm (spec name: `Promise Resolve Thenable Job`) checks for any object with a **callable `.then` method** at all, called a **thenable**, not specifically an instance of the built-in `Promise` class.

```js
const fakePromiseLike = {
  then(onFulfilled, onRejected) {
    setTimeout(() => onFulfilled("resolved from a thenable"), 100);
  },
};

Promise.resolve(1)
  .then(() => fakePromiseLike) // NOT a real Promise instance — just an object with .then()
  .then(value => console.log(value)); // "resolved from a thenable" — flattening still works
```

### Why this exists: interoperability with pre-native Promise libraries

Before native Promises existed, libraries like jQuery (`Deferred`) and early third-party Promise implementations (Bluebird, Q) all independently implemented Promise-like objects with a `.then()` method. Specifying the resolution algorithm around "does it have a callable `.then`" rather than "is it literally a `Promise` instance" meant native Promise chains could seamlessly consume values produced by any of these older libraries, without either side needing to know about the other's exact class.

### The mechanism, precisely

When a `.then()` callback returns a value, the chain's resolution logic checks: does this value have a `.then` property that's callable? If yes, it doesn't treat the value as the final result — it calls that `.then` method itself, passing in functions that will resolve/reject the chain's own next Promise once the thenable eventually calls them. This is a real, recursive check too: if THAT `.then` call itself resolves with another thenable, the process repeats — which is also the specific mechanism that guards against a pathological thenable whose `.then` resolves with itself, a scenario the spec explicitly detects and rejects with a `TypeError` rather than looping forever.

### Why this matters practically

Any object accidentally shaped like a thenable (has a `.then` property that happens to be a function, for reasons unrelated to Promises) will be treated as one by a Promise chain — a genuinely surprising source of bugs if, say, an object's `.then` property is coincidentally a business-logic method rather than Promise-resolution logic.

## 7. Common Pitfalls

1. **Forgetting to `return` inside a `.then()`** — if a callback doesn't return the inner Promise (or value), the next `.then()` receives `undefined` instead of waiting for it.
2. **Nesting instead of chaining** — reproduces callback hell inside Promise syntax; always flatten by returning the inner Promise.
3. **Missing a final `.catch()`** — an unhandled rejection anywhere in the chain will silently vanish (or trigger an `unhandledRejection` warning) if nothing downstream catches it.
4. **Assuming a `.catch()` mid-chain stops everything** — it only stops error propagation; execution continues normally with whatever the `.catch()` handler returns.

## 8. Quick Reference

```
.then(fn) returns a new Promise
returning a Promise from .then() → chain waits and flattens automatically
returning a plain value → wrapped automatically
throw inside .then() → skips to nearest .catch()
.catch() that doesn't re-throw → chain "recovers," continues normally
```
