skip to content

In JavaScript, what does calling .then() on a promise return, and how does the value your callback returns affect the next link in the chain?

level: juniorimportance: must knowfreq 78%

answer

  1. new object, not the same one
  2. the return value is the contract
  3. plain value versus promise
  4. adoption flattens automatically
  5. no return means undefined next

basics

~20 s

Every .then() call returns a brand-new promise. Returning a plain value from the callback fulfils that new promise with the value; returning a promise or thenable makes the new promise adopt that one's eventual outcome instead of nesting it.

solid answer

~40 s

`.then()` never mutates the promise you called it on — it registers a callback and returns a **new** promise, which is why chains compose. How that new promise settles depends entirely on the callback: return a plain value and the new promise fulfils with exactly that value; return a promise or any thenable and the new promise *adopts* it, staying pending until it settles and then taking its state and value; throw and the new promise rejects. Because of adoption you can never end up with a promise fulfilled by a promise — the machinery flattens automatically, so `.then(v => fetchMore(v)).then(v => ...)` sees the inner result, not a wrapper object. A callback with no `return` statement returns `undefined`, so the next link receives `undefined` immediately.

code

javascript · 13 lines
javascript
const p = Promise.resolve(1);
const q = p.then(v => v + 1);
console.log(p === q); // false: .then built a new promise

q.then(v => console.log('plain value:', v)); // 2

Promise.resolve('a')
  .then(v => Promise.resolve(v + 'b')) // returned promise is adopted
  .then(v => console.log('adopted:', v)); // "ab", not a Promise object

Promise.resolve('x')
  .then(v => { v + 'y'; }) // braced body, no return
  .then(v => console.log('no return:', v)); // undefined

go deeper

for a junior

Be able to say that .then() gives you a new promise and that the callback's return value becomes the next link's input. Know that a braced arrow body without return yields undefined.

for a middle

Explain adoption: returning a thenable makes the derived promise wait for it and take its outcome, so promises never nest. Be ready to trace a short chain and state each link's value.

for a senior

Show you use the contract deliberately — building pipelines where each step returns a promise, and distinguishing chaining from branching when deciding where errors and retries attach.

for a principal

Own the API-design angle: functions that return promises compose; functions that fire work and return undefined do not. Set the convention that every async helper returns its promise so callers can sequence and observe it.

## The shape of a chain A promise is an object standing in for a value that is not available yet. The method `.then(onFulfilled, onRejected)` registers callbacks to run when the promise settles, but the half people forget is its **return value**: `.then()` always constructs and returns a *new* promise, distinct from the receiver. The same is true of `.catch(fn)` — which is specified as `.then(undefined, fn)` — and of `.finally(fn)`. So `p.then(a).then(b).then(c)` is four promise objects, each downstream of the previous one. Calling `.then` on `p` does not change `p` at all; `p` remains settled with whatever it settled with, and the new promise carries the transformed outcome forward. ```js const p = Promise.resolve(1); const q = p.then(v => v + 1); console.log(p === q); // false — a new promise every time ``` ## What the callback returns decides everything When `p` fulfils with `value`, the engine calls `onFulfilled(value)`. Exactly three things can happen, and each maps to a settlement of the promise `.then` handed back: 1. **Return a non-thenable value** — a number, string, array, plain object, `undefined`. The new promise fulfils with exactly that value. 2. **Return a promise, or any thenable** (any object with a callable `then` method). The new promise does *not* fulfil with that object. It **adopts** it: it stays pending until the returned promise settles, then takes the same state and the same value or reason. 3. **Throw.** The new promise rejects with the thrown value. A callback whose body contains no `return` evaluates to `undefined`, which is case 1 — the next link runs right away with `undefined`. Arrow-function syntax makes this easy to get wrong: `v => doWork(v)` returns the result of `doWork`, while `v => { doWork(v); }` returns `undefined`, because a braced body needs an explicit `return`. ## Adoption, not nesting Adoption is the property that makes promises composable rather than a callback pyramid with extra steps. A promise can never be *fulfilled with* a thenable; the resolution machinery unwraps it first. That means one `.then` per asynchronous step, however deep the underlying calls go: ```js Promise.resolve('a') .then(v => Promise.resolve(v + 'b')) // adopted, not wrapped .then(v => console.log(v)); // "ab", never a Promise object ``` The flattening is also why `Promise.resolve(p)` returns `p` itself when `p` is already a native promise: there is nothing to wrap. Contrast this with a synchronous transform such as `Array.prototype.map`, where returning an array from the callback genuinely produces an array of arrays. Note that adoption is always asynchronous. Even `Promise.resolve(1).then(v => v)` does not run the callback synchronously — handlers are invoked after the current synchronous code finishes, so a chain never "returns" a value into surrounding code. ## Missing handlers pass through If you pass something that is not a function — `p.then(null)`, or `p.then(onFulfilled)` on a promise that rejects — that link is transparent. A fulfilment value flows past a link with no `onFulfilled`, and a rejection flows past a link with no `onRejected`, until it reaches a link that has one. This is why a single `.catch()` at the end of a long chain can handle a failure that happened at the top. ## Chaining versus branching Because `.then` returns a new promise, the *shape* of your calls matters: ```js p.then(a).then(b); // sequential: b receives a's result p.then(a); p.then(b); // two branches: a and b both receive p's value ``` The second form runs `a` and `b` independently on the same value, and a rejection handled on one branch is not handled on the other. Interviewers use this to check whether you think of `.then` as "attaching a listener" (branching) or as "building the next step" (chaining) — it is both, and the difference is which promise you keep calling `.then` on. ## Why interviewers ask it Almost every real promise bug reduces to this contract: something returned `undefined` when it meant to return a promise, or someone expected `.then` to hand a value back to the surrounding function, or someone re-used the original promise where they meant the derived one. Saying out loud "`.then` gives me a new promise, and my return value decides how it settles" is the answer the interviewer is listening for.

  • What is the difference between `p.then(a).then(b)` and calling `p.then(a)` and `p.then(b)` separately?
    The first is a chain: `b` runs on the promise `a` produced, so it receives `a`'s return value and waits for it. The second creates two independent branches off the same promise; `a` and `b` both receive `p`'s original value, run without ordering between them, and each branch settles and fails on its own.
  • Can a promise ever be fulfilled with another promise as its value?
    No. The resolution machinery unwraps thenables, so a fulfilment value is never a thenable. If you truly need to pass a promise around as data, hide it inside a non-thenable container, for example `{ inner: somePromise }` — that object has no callable `then`, so it is fulfilled as an ordinary value.
  • If a `.then` callback returns a value synchronously, does the next callback run synchronously too?
    No. Handlers are always invoked asynchronously, after the current synchronous execution finishes, even when the promise is already settled and the callback returns instantly. That is why you cannot read a chain's result into a surrounding variable and use it on the next line — the chain has not run yet.

saying these in an interview costs you the question

  • Thinking .then mutates and returns the same promise
  • Expecting .then to hand the value back synchronously
  • Believing a returned promise becomes a nested promise value
  • Forgetting a braced arrow body needs an explicit return
  • Assuming .then callbacks run immediately when the promise is already settled

context