Promise.prototype.then()

The primitive everything else is built on, including await. Two facts carry most of its behaviour: it always returns a new promise, and returning a promise from the callback flattens instead of nesting.

Promise methodES2015Live demo
Common call
p.then(v => transform(v))
Returns
a NEW promise — chainable
Replaces
callback-style continuation
Watch out
the second argument does NOT catch errors from the first
promise.then(onFulfilled[, onRejected])
→ Promise

Demo

Live evaluation
Try:
Inputs
nnumbera number (negative throws)
Output
await Promise.resolve(5).then(v => { if (v < 0) throw new Error('negative'); return v * 2; })
Pending…

then returns a new promise resolved with whatever the callback returns, so 5 becomes 10. The third case is the part people underestimate: a throw inside the callback does not escape as an exception — it becomes a rejection of the promise then returned, which only a catch AFTER this then will see. A second argument to the same then would not.

Parameters

NameTypeRequiredDescription
onFulfilledFunctionno (identity)Called with the resolved value. Its return value resolves the new promise; a promise returned here is flattened. Omitted, the value passes through.
onRejectedFunctionno (rethrow)Called if the ORIGINAL promise rejected. It does NOT see errors thrown by onFulfilled — that is the difference from a following catch.

Return value

Promise — A NEW Promise resolving to whatever the callback returned. If the callback returns a promise, it is flattened rather than nested.

Common patterns

Prefer await
Same semantics, far easier to read.
const v = await p;
const t = transform(v);
Chain transformations
Each then produces a new promise.
fetch(u).then(r => r.json()).then(d => d.items);
Catch after, not beside
A following catch sees errors from the callback too.
p.then(transform).catch(handle);

Examples

1. It returns a new promise
const p = Promise.resolve(1); p.then(() => {}) === p
Returns
false
2. Returned promises flatten
await Promise.resolve(1).then(() => Promise.resolve(2))
Returns
2
3. A following catch catches
await Promise.resolve(1).then(() => { throw new Error("in then"); }).catch(e => e.message)
Returns
'in then'
4. The second argument does NOT
Promise.resolve(1).then(() => { throw new Error("in then"); }, () => "never")
Returns
the returned promise REJECTS
5. onRejected sees the original
await Promise.reject(new Error("x")).then(() => "a", e => "handled: " + e.message)
Returns
'handled: x'
6. Omitting onFulfilled passes through
await Promise.resolve(1).then()
Returns
1

Pitfalls

1. The second argument cannot catch the first
onRejected only fires for a rejection of the ORIGINAL promise. An error thrown inside onFulfilled bypasses it entirely and rejects the returned promise, so then(fn, handler) is not equivalent to then(fn).catch(handler) — the second form is almost always what you want.
Not caught
p.then(() => { throw new Error("x"); }, e => "handled")
the result promise rejects
catch after
p.then(() => { throw new Error("x"); }).catch(e => "handled")
'handled'
2. Forgetting to return from the callback
A braced callback with no return resolves the new promise with undefined, so the next link in the chain receives nothing. Arrow functions with a body make this easy to do while looking correct.
Resolves undefined
p.then(v => { transform(v); }).then(t => t)
undefined
Return it
p.then(v => { return transform(v); }).then(t => t)
the transformed value
3. Not returning the inner promise breaks the chain
Starting async work inside a then without returning its promise detaches it — the chain continues immediately and the work becomes a floating promise whose rejection is unhandled. The same mistake as forgetting await.
Detached
p.then(() => { save(); }).then(() => "done")
"done" before save finishes
Return it
p.then(() => save()).then(() => "done")
after save finishes
4. Callbacks never run synchronously
Even on an already-resolved promise, the callback is queued as a microtask. Code written to read a variable immediately after attaching a then sees the value before the callback ran.
Reads too early
let x;
Promise.resolve(1).then(v => { x = v; });
x
undefined
Await it
const x = await Promise.resolve(1);
x
1

When to use

Use it
  • Interoperating with promise-returning APIs in non-async code
  • Attaching a handler without making the surrounding function async
  • Short transformation chains where await would need an extra function
Reach for something else
  • Ordinary sequential async logic → await, which reads far better
  • Error handling → catch after, not the second argument
  • You need every result of several promises → Promise.all
  • The callback starts async work → return its promise

Notes

Complexity
O(1) to attach; the callback runs as a microtask
Return
A new Promise; the original is unchanged and can be thened again
CPython impl
V8: Builtins-promise-then
Memory
Allocates a new promise per call — long chains allocate per link
Thread-safe
Single-threaded; callbacks run on the microtask queue

FAQ

Almost always the second. The two-argument form only handles rejections from the ORIGINAL promise, so an error in fn escapes it. A following catch covers both, which is what people expect from the name.

p.then(fn).catch(handler);   // covers errors in fn too

History

ES2015
Promise with then, catch and the thenable protocol standardised from the Promises/A+ specification.
ES2017
async/await added as syntax over the same machinery.