Nick Manning
Nick Manning

Reputation: 2989

Unexpected behavior mixing process.nextTick with async/await. How does the event loop work here?

My code:

async function run(){

    process.nextTick(()=>{
        console.log(1);
    });

    await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));

    console.log(3);

    process.nextTick(()=>{
        console.log(4);
    });

    new Promise(resolve=>resolve()).then(()=>{console.log(5)});

}

run();

My expected output is the numbers to print 1,2,3,4,5 in order, but instead I get:

1
2
3
5
4

As expected, the first nextTick is evaluated before the first .then callback, because process.nextTick and .then are both deferred to future ticks, and process.nextTick is declared before .then. So 1 and 2 are outputted in order as expected.

The code should not reach what is after await until after .then is resolved, and this works as expected, as 3 is outputted in the expected place.

Then essentially we have a repeat of the first part of the code, but this time .then is called before process.nextTick.

This seems like inconsistent behavior. Why does process.nextTick get called before the .then callback the first time around but not the second?

Upvotes: 0

Views: 1274

Answers (2)

jfriend00
jfriend00

Reputation: 707446

The node.js event queue is not a single queue. It is actually a bunch of different queues and things like process.nextTick() and promise .then() handlers are not handled in the same queues. So, events of different types are not necessarily FIFO.

As such, if you have multiple things that go in the event queue around the same time and you want them served in a specific order, the simplest way to guarantee that order is to write your code to force the order you want, not to try to guess exactly how two things are going to get sequenced that went into the queue around the same time.

It is true that two operations of the exact same type like two process.nextTick() operations or two resolved promise operations will be processed in the order they were put into the event queue. But, operations of different types may not be processed in the order relative to each other that they were put in the event queue because different types of events are processed at different times in the cycle the event loop makes through all the different types of events.

It is probably possible to fully understand exactly how the event loop in node.js works for every type of event and predict exactly how two events that enter the event queue at about the same time will be processed relative to one another, but it is not easy. It is further complicated by the fact that it also depends upon where the event loop is in its current processing when the new events are added to the event queue.

As in my delivery example in my earlier comments, when exactly a new delivery will be processed relative to other deliveries depends upon where the delivery driver is when the new order arrives in the queue. The same can be true of the node.js event system. If a new event is inserted while node.js is processing a timer event, it may have a different relative order to other types of events than if it node.js was processing a file I/O completion event when it was inserted. So, because of this significant complication, I don't recommend trying to predict the execution order of asynchronous events of different types that are inserted into the event queue at about the same time.

And, I should add that native promises are plugged directly into the event loop implementation (as their own type of micro task) so a native promise implementation may behave differently in your original code than a non-native promise implementation. Again a reason not to try to forecast exactly how the event loop will schedule different types of events relative to one another.


If the order of processing is important to your code, then use code to enforce a specific completion processing order.

As an example of how it matters what the event queue is doing when events are inserted into the event queue, your code simplified to this:

async function run(){

    process.nextTick(()=>{
        console.log(1);
    });

    await Promise.resolve().then(()=>{console.log(2)});

    console.log(3);

    process.nextTick(()=>{
        console.log(4);
    });

    Promise.resolve().then(()=>{console.log(5)});

}

run();

Generates this output:

1
2
3
5
4

But, simply change when the run() is called to be Promise.resolve().then(run) and the order is suddenly different:

async function run(){

    process.nextTick(()=>{
        console.log(1);
    });

    await Promise.resolve().then(()=>{console.log(2)});

    console.log(3);

    process.nextTick(()=>{
        console.log(4);
    });

    Promise.resolve().then(()=>{console.log(5)});

}

Promise.resolve().then(run);

Generates this output which is quite different:

2
3
5
1
4

You can see that when the code is started from a resolved promise, then other resolved promises that happen in that code get processed before .nextTick() events which wasn't the case when the code was started from a different point in the event queue processing. This is the part that makes the event queue system very difficult to forecast.


So, if you're trying to guarantee a specific execution order, you have to either use all the same type of events and then they will execute in FIFO order relative to each other or you have to make your code enforce the execution order you want. So, if you really wanted to see this order:

1
2
3
4
5

You could use all promises which would essentially map to this:

async function run(){

    Promise.resolve().then(() => {
        console.log(1);
    })
    Promise.resolve().then(() => {
        console.log(2)
    });

    await Promise.resolve().then(()=>{});

    console.log(3);

    Promise.resolve().then(() => {
        console.log(4)
    });

    Promise.resolve().then(()=>{console.log(5)});

}

run();

Or, you change the structure of your code so the code makes it always process things in the desired order:

async function run(){

    process.nextTick(async ()=>{
        console.log(1);
        await Promise.resolve().then(()=>{console.log(2)});
        console.log(3);
        process.nextTick(()=>{
            console.log(4);
            Promise.resolve().then(()=>{console.log(5)});
        });
    });
}

run();

Either of these last two scenarios will generate the output:

1
2
3
4
5

Upvotes: 1

Nick Manning
Nick Manning

Reputation: 2989

Thanks to the helpful comments that made me realize that not all javascript ways of deferring to a later tick are created equal. I had figured that (new Promise()).then, process.nextTick, even setTimeout(callback,0) would all be exactly the same but it turns out I can't assume that.

For now I'll leave here that the solution to my problem is simply to not use process.nextTick if it does not work in the expected order.

So I can change my code to (Disclaimer this is not actually good async code in general):

async function run(){

  process.nextTick(()=>{
    console.log(1);
  });

  await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));

  console.log(3);

  (new Promise(resolve=>resolve())).then(()=>{console.log(4)});

  (new Promise(resolve=>resolve())).then(()=>{console.log(5)});

 }

run();

Now I'm ensuring that 4 is logged before 5, by making both of them the same type of async call. Making both of them use process.nextTick would also ensure that 4 logs before 5.

async function run(){

  process.nextTick(()=>{
   console.log(1);
  });

  await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));

  console.log(3);

  process.nextTick(()=>{
    console.log(4)
  });

  process.nextTick(()=>{
    console.log(5)
  });

}

run();

This is a solution to my problem, but if anyone wants to provide a more direct answer to my question I'll be happy to accept.

Upvotes: 0

Related Questions