Promise Chaining
Introduction
Promise chaining is not just calling .then repeatedly. Every .then call returns a new promise. The callback's return value determines that new promise: a plain value fulfils it, a thrown error rejects it, and a returned promise or thenable is flattened into it.
This is how promise chains express async pipelines without nested callback pyramids.
Why This Matters
Chaining tests whether you understand flattening, error propagation, and sequencing. A senior answer should explain why returning a promise makes the next link wait, while starting async work without returning it breaks the chain.
Theory
Every .then returns a new promise
.then(onFulfilled, onRejected) attaches handlers to one promise and immediately returns another promise. That returned promise represents the result of whichever handler eventually runs. The original promise is not mutated.
| Handler behavior | Next promise state |
|---|---|
| Returns a plain value | Fulfilled with that value. |
| Returns nothing | Fulfilled with undefined. |
| Throws an error | Rejected with that error. |
| Returns a fulfilled promise | Fulfilled with that promise's value. |
| Returns a rejected promise | Rejected with that promise's reason. |
| Returns a pending promise | Waits, then adopts its outcome. |
Flattening
When a handler returns a promise, the next .then receives the eventual value, not a promise wrapper. This unwrapping is why return loadUser().then(...) composes cleanly.
The classic bug is forgetting return. If a handler starts async work but returns nothing, the next link receives undefined immediately and the chain no longer waits for that work.
Missing handlers pass through
A .then with no fulfillment handler passes fulfillment through unchanged. A .then with no rejection handler passes rejection through unchanged. This is why a final .catch can handle errors thrown anywhere above it.
Sequential vs parallel
Chains are sequential by default. If step 2 depends on step 1, chaining is perfect. If operations are independent, start them together and combine them with Promise.all; otherwise you accidentally serialize work.
Microtask boundaries
Each reaction runs in a microtask. Returning a plain value queues the next link as another microtask in the same checkpoint. Returning a timer-backed promise pauses the chain until a future macrotask settles it.
Visual Diagrams
p0 fulfilled with 2
|
v
then returns p1 -- handler returns 6
|
v
then returns p2 -- handler returns delayed promise
|
v
then returns p3 -- handler receives delayed value
Each step creates a new promise.The chain transforms outcomes without mutating previous promises.
Code Examples
Return values become the next value
A plain return value fulfils the promise returned by .then.
Returned promises are flattened
The second handler receives the eventual string, not a nested promise object.
Forgetting return breaks sequencing
The outer chain does not wait for saveAuditLog because its promise is not returned.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1Promise.resolve('start')2 .then(function (value) {3 console.log(value);4 return 'middle';5 })6 .then(function (value) {7 console.log(value);8 return Promise.resolve('end');9 })10 .then(function (value) {11 console.log(value);12 });13 14console.log('sync');Predict the output #2
1Promise.resolve()2 .then(function () {3 console.log('A');4 return new Promise(function (resolve) {5 setTimeout(function () {6 console.log('B');7 resolve('C');8 }, 0);9 });10 })11 .then(function (value) {12 console.log(value);13 });14 15setTimeout(function () {16 console.log('D');17}, 0);18 19console.log('E');Coding Exercises
Run promise-returning tasks sequentially
MediumImplement runSequentially(tasks), where each task is a function returning a value or promise. Run tasks one after another and resolve to an array of results. Reject immediately if any task rejects.
Interview Questions
1What does `.then` return, and how is that promise resolved?
.then always returns a new promise. If the chosen handler returns a plain value, the new promise fulfils with it. If the handler throws, the new promise rejects. If the handler returns a promise or thenable, the new promise adopts that outcome.
Follow-ups
- What happens if the handler returns nothing?
- Why does forgetting `return` break sequencing?
2How do you avoid promise nesting when one async step depends on another?
Return the inner promise from the .then handler. The next link in the outer chain waits for it because promise resolution flattens returned promises.
Quiz
1. Inside a `.then` handler, what happens when you `return Promise.resolve(42)`?
2. What is the result of a `.then` handler that completes without returning?
Summary
- Every `.then` returns a new promise and does not mutate the original.
- Plain return values fulfil the next promise; throws reject it; returned promises are flattened.
- Forgetting to return async work causes the chain to continue too early with `undefined`.
- Chains are sequential; use combinators for independent parallel work.
Cheat Sheet
.then returns: a new promise.
Return value: next promise fulfils with it.
Throw: next promise rejects.
Return promise: next promise adopts it.
No return: next value is undefined.
Rule: return async work when the chain must wait.