Case 4/13

Prefer reading? The full explanation

How Promise Chains Run

Two chains, interleaved · Browser

Before this

How Promises Run — the microtask queue.

After this you can explain

Why two independent .then() chains interleave instead of running one after the other.

Promise.resolve()
  .then(() => console.log('A1'))
  .then(() => console.log('A2'));

Promise.resolve()
  .then(() => console.log('B1'))
  .then(() => console.log('B2'));

Output: A1 → B1 → A2 → B2

Why do the two chains interleave?

The output is A1 → B1 → A2 → B2. The intuitive answer, A1 → A2 → B1 → B2, assumes that once a chain starts it runs to the end. Promise chains do not work that way.

Running the two statements queues exactly two callbacks: A1 and B1. A2 and B2 do not exist in the queue yet, because a chained .then() attaches to the promise returned by the previous .then(), and that promise has not settled. Registering a handler on an unsettled promise queues nothing.

Each link costs one turn

When A1 runs and returns, the promise it belongs to settles, and only then is A2 queued. At that moment B1 is already in the queue, so A2 lands behind it. B1 then runs and queues B2 behind A2.

The result is that both chains advance exactly one link per turn through the queue. A chain of three .then() calls needs three separate turns, which is why a long chain can be overtaken by shorter work started later.

Chaining versus branching

The experiment below attaches two handlers to the same resolved promise instead of chaining them. Both queue immediately during the synchronous phase, and both run in the first drain, because neither is waiting on the other to settle.

That is the practical distinction. p.then(f).then(g) means "g after f", and costs two turns. p.then(f); p.then(g) means "f and g both when p settles", and costs one.

When the interleaving matters

All four callbacks here run inside a single microtask drain, before any timer or repaint. So interleaving is rarely a performance issue; it is an ordering issue. Code that assumes one chain completes before another begins will read stale state or write in an unexpected order.

If two sequences genuinely must not interleave, express that dependency directly by chaining them together or awaiting one before starting the other, rather than relying on source order.

Reading about it is one thing. Watching it run is another.

↑ Run it step by step

In the wild

Where this rule bites real code

The two initialisers that interleave

Symptom
Two startup chains — load config, load user — each work alone. Together, the user chain sometimes reads half-initialised config.
Why
Independent chains advance one link per microtask turn, interleaved. Nothing about source order makes chain A finish before chain B starts its next link.
Fix
Express real dependencies as real chains: await loadConfig() before starting what needs it. If they are genuinely independent, they must not share mutable state mid-flight.

The forgotten return that broke sequencing

Symptom
You chain .then(() => save()).then(() => notify()) and notify() sometimes runs before save() finished.
Why
The first callback CALLS save() but does not return its promise, so the chain link settles immediately with undefined instead of waiting.
Fix
Return every promise you create inside a .then — or use async/await, where forgetting the await is at least easier to spot in review.

Environment

Browser

CI verified

Node.js v22.22.3

Last reviewed

29 July 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →