javascriptnode.jsevent-loop

process.nextTick vs microtask execution order


I have following code snippet in node js


Promise.resolve().then(() => {
  Promise.resolve().then(() => console.log('promise'));
  process.nextTick(() => console.log('nextTick'));
});

// promise
// nextTick

And I have this snippet

Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

// nextTick
// promise

From the documentation of nodejs, I have read that all callbacks passed to process.nextTick() will be resolved before the event loop continues.

It is clear to me that in the second example next tick is executed first since it executes before each iteration of event loop. But why in the first example it executes after the promise.resolve()?


Solution

  • Preface: This is only something we can reason about because we know the inner promise is already fulfilled as of when the fulfillment handler is attached to it via then. But in general, avoid assuming you know a promise's state until your code observes it. If the inner promise weren't fulfilled until an event occurred, the nextTick handler would be called first.


    To understand why that second fulfillment handler was called before the nextTick callback, let's look at what the event loop guide says about nextTick:

    You may have noticed that process.nextTick() was not displayed in the diagram, even though it's a part of the asynchronous API. This is because process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.

    (my emphasis)

    Let's look at what the code above does:

    I can't say I've delved into the relevant Node.js code, but it looks like there is no "operation transition" involved. The microtask queue is processed until it's empty at the end of every JavaScript task.¹ In this case, the task is the main script execution; the loop emptying the microtask queue is at the end of that task. It picks up the microtask that the main script queued, and in the process of running that microtask it queues another microtask (the fulfillment handler logging "promise"). Since the loop runs until the microtask queue is empty, it performs that second microtask before transitioning back to the C/C++ handler.

    Again, though, we can only reason about this in this way because we know (from looking at the code) what the state of that inner promise is when the handler is attached to it, which we generally won't know. :-)


    ¹ I should note that as far as I know this isn't specified behavior for Node.js (it is on the web platform — thanks kaiido for that), but given that the Node.js project team are trying to align with the web platform where possible, it's unlikely Node.js would go off in its own way on this. But for the moment, the Node.js event loop documentation above doesn't talk about microtasks, unfortunately.

    Just to demonstrate an extreme example, here's an example of some Truly Awful code starving the event loop for 20 seconds by repeatedly queuing microtasks, prevent the nextTick handler scheduled before the microtasks from running until the microtasks stop queuing new microtasks:

    // Starving the event loop for 20 seconds by scheduling microtasks
    // Obviously, don't do this! :-)
    console.log(new Date().toISOString());
    let count = 0;
    const stop = Date.now() + 20000;
    const handler = () => {
        if (Date.now() < stop) {
            ++count;
            Promise.resolve().then(handler);
        }
    };
    // Inside a microtask...
    Promise.resolve().then(() => {
        // ...schedule next tick callback...
        process.nextTick(() => {
            console.log(new Date().toISOString());
            console.log(count);
        });
        // ...and queue nested microtasks until 20 seconds have passed
        handler();
    });