1

SYNOPSIS:

In Node.js event queues, and code like "new Promise((r) => setTimeout(r, t));", is the setTimeout() evaluated NOW, in the microqueue for Promise resolves, or where?

DETAILS:

I'm reading through Distributed Systems with Node.js (Thomas Hunter II, O'Reilly, 3rd release of First Edition). It tells me that Node.js goes thru each queue in turn:

  • Poll: for most things, including I/O callbacks
  • Check: for setImmediate callbacks
  • Close: when closing connections
  • Timers: when setTimeout and setInterval resolve
  • Pending: special system events

There are also two microqueues evaluated after each queue is empty, one for promises and one for nextTick().

On the book's p.13 he has an example where an await calls a function that returns "new Promise((r) => setTimeout(r, t));". The book code is:

const sleep_st = (t) => new Promise((r) => setTimeout(r, t));
const sleep_im = () => new Promise((r) => setImmediate(r));
  
(async () => {
  setImmediate(() => console.log(1));
  console.log(2);
  await sleep_st(0);
  setImmediate(() => console.log(3));
  console.log(4);

That is,

setImmediate(() => console.log(1));
console.log(2);
Promise.resolve().then(() => setTimeout(() => {
  setImmediate(() => console.log(3));
  console.log(4);

This is what I think is going on:

  1. The program starts with a task in the Poll queue, the p.13 code. It starts running.

  2. The Check queue gets a task and the "2" printed to the console.

  3. The "await sleep_st(0)" will have called setTimeout, which puts a task on the Timer queue. Since the timeout is zero, by the time we access the Timer queue there will be work to do. The sleep_st(0) returns a Promise.

  4. This ends the work of the Poll queue.

  5. Now the result micro queue starts. My code resumes executing. This should start with setImmediate() and console.log(4).

This means that the output to the console is "2 4". However, the book says the proper sequence is "2 1 4 3". That is, the event queue for Check, and perhaps Timer, gets involved.

What, then, happens in the promise result microqueue?

3
  • 1
    Possible duplicate: stackoverflow.com/questions/49685779/… Commented Sep 15, 2021 at 15:43
  • Not sure how you arrived at Promise.resolve().then(() => setTimeout(() => {. Did you mean sleep_st(0).then(() => { in that line? Commented Sep 15, 2021 at 19:06
  • The code of "how you arrived at" is transcribed from page 13 of the book. The author presented the logic both ways. Commented Sep 15, 2021 at 20:30

2 Answers 2

3

In Node.js event queues, and code like "new Promise((r) => setTimeout(r, t));", is the setTimeout() evaluated NOW, in the microqueue for Promise resolves, or where?

The call to setTimeout is evaluated "now." (The setTimeout callback is called later as appropriate, during the time phrase.) When you do new Promise(fn), the Promise constructor calls fn immediately and synchronously, during your call to new Promise(fn). This is so the function (called an executor function) can start the asynchronous work that the promise will report on, as your two examples (one starts the work by calling setTimeout, the other by calling setImmediate.)

You can easily see this with logging:

console.log("Before");
new Promise((resolve, reject) => {
    console.log("During");
    setTimeout(() => {
        console.log("(fulfilling)");
        resolve();
    }, 10);
})
.then(
    () => {
        console.log("On fulfillment");
    },
    () => {
        console.log("On rejection");
    }
);
console.log("After");

That logs

Before
During
After
(fulfilling)
On fulfillment

because

  • It calls console.log("Before"); before doing anything else.
  • It calls new Promise and log console.log("During"); synchronously in the callback.
  • It calls console.log("After"); after creating the promise and adding fulfillment and rejection handlers to it.
  • It calls console.log("(fulfilling)"); when the timer fires and fulfill the promise.
  • It calls console.log("On fulfillment"); when the fulfillment handler is called.

On your notes on the sequence:

  • The "await sleep_st(0)" will have called setTimeout

Just to be really clear, it's specifically the sleep_st(0) part that called setTimeout. All await did was wait for the promise sleep_st returned after calling setTimeout to settle.


You may find this example useful, see inline comments:

const sleep = ms => new Promise(resolve => {
    // Happens immediately and synchronously when `sleep` is called
    console.log("calling setTimeout");
    setTimeout(() => {
        // Happens later, during the timer phase
        console.log("fulfilling promise");
        resolve(); // <=== If there are any attached promise handlers,
                   //      this queues calls to them in the microtask
                   //      queue, to be done after this "macro" task
                   //      running in the timer phase is complete
    }, ms);
});

const example = async (label) => {
    // Happens synchronously and immediately when `example` is called
    await sleep(0);
    // Happens in a microtask queued by the fulfillment of the promis
    console.log(`Sleep done: ${label}`);
};

(async () => {
    await Promise.all([example("a"), example("b")]);
    // Happens in a microtask queued by fulfillment of the `Promise.all`
    // promise
    console.log("All done");
})();

The output is:

calling setTimeout
calling setTimeout
fulfilling promise
Sleep done: a
fulfilling promise
Sleep done: b
All done

Note how the code for Sleep done: a was executed between the two tasks for the timer callbacks, because those timer callbacks are "macro" tasks and promise fulfillment callbacks are queued as microtask to be run at the end of the current macrotask.

Sign up to request clarification or add additional context in comments.

3 Comments

Answering my Q in the form I asked it is most gratifying, and clear to me. The book says "Callbacks in the microtasks queues take priority over callbacks in the phase's normal queue, and callbacks in the next tick microtask queue run before callbacks in the promise microtask queue." I took that as "microtask runs after each queue is empty", but properly is "microtasks are run after each queue task, even if there are tasks remaining in that queue. Thus, the resolve() is run interstitially in the Timer queue.
Regarding T.J.'s own example, it seems to me that when the Timer queue is accessed there are two tasks on the queue, timeout fcns for example "a" and "b". The "a" timeout fcn is called, which itself calls resolve(). The "a" timeout fcn exits, and now the microqueues are consulted. There is that resolve, and execution goes past the "await sleep(0)" and logs "sleep done a". Now that the microqueue is empty (there are no nextTick() events), the other Timer task is called, for example "b". Or so I think. If I'm right then I can mark the Q as answered :-)
@Jerome - Yes, that's basically it. It's not the rersolve that's put in the microtask queue, but any fulfillment handler calls that fulfilling the promise needs to trigger. But Yes, two timer tasks in the queue when Node enters the timer phase; it runs the first one runs, which calls resolve; since there is a fulfillment handler waiting to be run, it queues that as a microtask; at that point the first timer task is done and so the microtask it queued gets run; after that, the next timer task gets run. The key bit, as you said, is that microtasks run at the end of the task that queued them.
0

by the time we access the Timer queue there will be work to do

Before get to timer phase, there is check phase, so "1" is printed before "3", but i exectued the code on my window, the result is 2 4 1 3, that is setTimeout and setImmediate will race for exectued, sometime setTimeout first, sometile second, i executed the example code in this book for many time in a short time

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.