4

I recently encountered this question on Stack Overflow, asking about how the then method really works in JavaScript. The responder, Trincot, made the following comment:

The host will check which job queues have entries, giving precedence to job queues with high priority. A Promise job queue has a very high priority, typically higher than the event queue that deals with user interaction or other external events. So, the job that was placed in the queue at step 4 above is taken out of the Promise Job queue. This job will sequentially call the callback functions that have been registered as then callbacks (such as the one registered in step 7) on the myPromise object.

If I understood his explanation correctly, whenever the resolve function is invoked, JavaScript will schedule a microtask (or a job) in the job queue, which will execute all the callbacks passed to then, including chaining, when the call stack is empty. For example:

Promise.resolve("Hi")
    .then(() => {
        console.log("Hi");
    })
    .then(() => {
        console.log("World");
    });

In this case, the promise is resolved immediately, which means that a microtask will be scheduled immediately. When the then callbacks finish execution, the JS engine will check for any microtasks in the queue. Since the promise was resolved immediately, it will execute the microtask, which in turn executes all the handlers passed to the then method. As a result, the code will output "Hi" and "World".

However, why does this code output "1 3 2 4" instead of "1 2 3 4"?

const p1 = Promise.resolve();
const p2 = Promise.resolve();
p1.then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
});
p2.then(() => {
    console.log(3);
}).then(() => {
    console.log(4);
});

I believe the code should output "1 2 3 4" instead of "1 3 2 4". This is because, when p1 and p2 resolve, two microtasks are enqueued. As the then callbacks finish execution, they add these callbacks to their internal lists. Once the call stack is empty, the JS engine selects the oldest task from the queue and executes it. This task should execute all the then handlers passed to that specific Promise instance. However, it doesn't seem to be happening as expected. Could someone explain to me the reason behind this behavior?

Thanks for your time and have a great day!

InSync
  • 4,851
  • 4
  • 8
  • 30
Toby Harnish
  • 90
  • 1
  • 9
  • I may be wrong but firstly 1 and 3 are scheduled then 1 is resolved which schedules 2 and 3 is resolved which schedules 4 – Konrad Aug 19 '23 at 20:40
  • @Konrad, can you explain why? – Toby Harnish Aug 19 '23 at 20:41
  • "*a microtask, which will execute all the callbacks passed to then, including chaining*" - it's not clear what you mean by "chaining" here. Each microtask executes only a single callback (that was registered on the promise that is being resolved, or that is being passed to `then` on an already-resolved promise) – Bergi Aug 19 '23 at 21:17
  • @Bergi, It just means multiple 'then' chained together. – Toby Harnish Aug 19 '23 at 21:19
  • But no, you correctly explained that "*[the] task should execute all the then handlers passed to that specific Promise instance.*" (actually it's one microtask per handler, but your example has only a single handler per promise anyway). In a chain, you have multiple separate promises, and each has its own task(s)! – Bergi Aug 19 '23 at 21:23
  • Please note I have updated my answer in the linked Q&A, as that was not well phrased. When a promise is resolved, it could be that 0, 1 or more jobs are placed on the job queue -- this depends on whether at the moment of resolving there were already `then` callbacks registered. – trincot Aug 19 '23 at 21:51
  • @TobyHarnish Each `.then()` call will schedule its own microtask. – Bergi Aug 21 '23 at 01:43

2 Answers2

4

when p1 and p2 resolve, two microtasks are enqueued.

That is a misunderstanding. A microtask (a promise job) is enqueued when:

  • a then method is executed on a resolved promise
  • a promise resolves and there was already a then method executed on it before it resolved: for each such then callback a separate job is created.

So when the first two statements have executed, there is nothing added to the promise job queue yet.

Realise that:

  • when a then method executes, its callback is put on the promise job queue only when the promise was already resolved. Otherwise it is registered in wait for the promise to resolve.
  • When a then method is executed, it returns a promise that is always pending, even when it is called on a resolved promise.

It may help to identify the promises returned by then() calls with a variable, and also name the callback functions.

This would make your script look like this:

const p1 = Promise.resolve();
const p2 = Promise.resolve();
const q1 = p1.then(function p1_then() { console.log(1) });
const r1 = q1.then(function q1_then() { console.log(2) });
const q2 = p2.then(function p2_then() { console.log(3) });
const r2 = q2.then(function q2_then() { console.log(4) });

Here is a simplified view on what happens in that script:

  • The context identifies from where the current action is taken
  • The action is the current statement that executes
  • Each named promise has its own column: ? means it is pending; F means it is fulfilled.
  • The last column lists the current entries in the Promise Job Queue
Context Action p1 p2 q1 r1 q2 r2 Promise Job Queue
Script p1 = Promise.resolve() F - - - - - -
p2 = Promise.resolve() F F - - - - -
q1 = p1.then(p1_then) F F ? - - - p1_then
r1 = q1.then(q1_then) F F ? ? - - p1_then
q2 = p2.then(p2_then) F F ? ? ? - p1_then,p2_then
r2 = q2.then(q2_then) F F ? ? ? ? p1_then,p2_then
Host get job p1_then F F ? ? ? ? p2_then
p1_then console.log(1) F F ? ? ? ? p2_then
p1_then (return) F F F ? ? ? p2_then,q1_then
Host get job p2_then F F F ? ? ? q1_then
p2_then console.log(3) F F F ? ? ? q1_then
p2_then (return) F F F ? F ? q1_then,q2_then
Host get job q1_then F F F ? F ? q2_then
q1_then console.log(2) F F F ? F ? q2_then
q1_then (return) F F F F F ? q2_then
Host get job q2_then F F F F F ? -
q2_then console.log(4) F F F F F ? -
q2_then (return) F F F F F F -
Host nothing to do F F F F F F -
trincot
  • 317,000
  • 35
  • 244
  • 286
  • What do you mean by "a promise resolves and there was already a then method executed on it before it resolved"? – Toby Harnish Aug 19 '23 at 21:21
  • 2
    If you have a pending promise and call `then` on it, nothing gets put in a job queue yet. But if then later that promise gets resolved, it is at that moment the `then` callback will be put on the job queue. – trincot Aug 19 '23 at 21:37
-2

Here's the order of execution:

  1. The .then() handler for p1 logs 1 to the console.
  2. The .then() handler for p2 logs 3 to the console.
  3. The chained .then() handler for p1 logs 2 to the console.
  4. The chained .then() handler for p2 logs 4 to the console.

so the order of process will be 1,3,2,4

Remember that while the code appears synchronous, Promise resolution and the execution of .then() handlers are asynchronous processes.

Hard
  • 105
  • 5