Order of Execution of JavaScript Promises (With Examples)
By: Matt Lim
Tags: Asynchronous, JavaScript, Promises

Example #3 — Interleaved execution

Code

import {

addLoggingToPromiseThen,

getFulfilledPromise,

logThen,

} from "../util.js"; addLoggingToPromiseThen(); const prom1 = getFulfilledPromise("prom1");

const prom2 = prom1.then(logThen.bind(null, "1"));

const prom3 = prom2.then(logThen.bind(null, "2")); const prom4 = getFulfilledPromise("prom2");

const prom5 = prom4.then(logThen.bind(null, "3"));

const prom6 = prom5.then(logThen.bind(null, "4"));

Logs

Promise executor, result will be prom1

In Promise.prototype.then Promise { 'prom1' }

In Promise.prototype.then Promise { <pending> }

Promise executor, result will be prom2

In Promise.prototype.then Promise { 'prom2' }

In Promise.prototype.then Promise { <pending> }

[1] Promise then, result = prom1

[3] Promise then, result = prom2

[2] Promise then, result = prom1

[4] Promise then, result = prom2

Explanation

We can understand this using rules #1, #2, #3, #4, and #5. This one will be a bit more tricky because of rule #3…

First, note that getFulfilledPromise("prom1").then(...) does not return a fulfilled promise. We can confirm this by looking at the logs from Promise.prototype.then — only the initial call to then for each promise logs a fulfilled promise.

So, let’s go through this line-by-line and see what happens.

const prom1 = getFulfilledPromise("prom1"); . This just gets a fulfilled promise. const prom2 = prom1.then(logThen.bind(null, "1")); . Here, .then() is called on a fulfilled promise, which means that the corresponding job gets immediately enqueued (we also saw this in the last example). The call returns a pending promise that will get fulfilled when the enqueued job runs. That is, the job will run our onFulfilled callback, which is logThen.bind(null, "1") , and it will fulfill prom2 . Note that jobs will only start running after the script completes (spec 8.4)! See spec 26.6.5.4.1 to read more about the details. The main important part is this line:

“Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).”



At this point, here is the state of things. "1" is shorthand for “the job that runs logThen.bind(null, "1") .

job_queue = ["1"] const prom3 = prom2.then(logThen.bind(null, “2”)); . Here, .then() is called on a pending promise. Spec 26.6.5.4.1 describes the behavior in this scenario. Here’s the relevant line:

“Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]].”

In simple terms, this makes it so that when prom2 gets fulfilled, logThen.bind(null, "2")) will be called.



At this point, here is the state of things.

job_queue = ["1"]

prom2_fulfill_reactions = ["2"] const prom4 = getFulfilledPromise("prom2"); . This is basically the same as #1, it just gets a fulfilled promise. const prom5 = prom4.then(logThen.bind(null, "3")); . This is similar to #2. Since .then() is called on a fulfilled promise, the corresponding job gets immediately enqueued. Just as before, the job will run our callback and fulfill the promise returned by the call to .then() (which is prom5 in this case).



At this point, here is the state of things.

job_queue = ["1", "3"]

prom2_fulfill_reactions = ["2"] const prom6 = prom5.then(logThen.bind(null, "4")); . This is basically the same as #3. However, this time, we add a reaction to the list for prom5 , not prom2 .



At this point, here is the state of things.

job_queue = ["1", "3"]

prom2_fulfill_reactions = ["2"]

prom5_fulfill_reactions = ["4"]

Alright, that’s it for the execution of the script itself. After the script runs, jobs from the job queue will start running. Here’s how that goes.

Jobs get run in the order they were enqueued, so the job we’ve named "1" will execute first. This explains why [1] Promise then, result = prom1 is the first line that’s logged. Remember that this job not only runs our callback, but also fulfills prom2 . If we take a look at spec 26.6.1.4 and spec 26.6.1.8, we can see that fulfilling a promise enqueues a job for each element in its “fulfill reactions” list. So, after this job runs, here is the state of things.

job_queue = ["3", "2"]

prom5_fulfill_reactions = ["4"]

Job "1" gets run and popped from the queue and job "2" gets enqueued.

The next job in the queue is "3" , so that’s the next job that will execute. Just as with the first job, it will run the callback (thus logging [3] Promise then, result = prom2 ) and enqueue job "4" . After this job runs, here is the state of things.

job_queue = ["2", "4"]

It should be clear what happens at this point :).