#100DaysOfMERN - Day 8

#100DaysOfMERN - Day 8

·

9 min read

✏ Understanding setImmediate()

Yesterday, I tested my understanding of what's happening when an asynchronous operation gets called in the synchronous flow of my code. To do so, I wrote a number of functions that all console.log something, with each of them being called in a different manner (synchronous or asynchronous).

All went well until I met Node's setImmediate(), which showed very odd behaviour.

Here's the list of my test functions:

// log synchronously without delay
function log(){
    console.log('log')
}

// log synchronously, but with a ~1.5 second delay
function log_loopDelay(){
    let i=0;
    while(i < 1E9) {i++};
    console.log('loopDelay')
}

// log asynchronously (with adjustable time parameter)
function log_setTimeout(t){
    setTimeout(() => console.log(`setTimeout(${t}s)`), t*1000)
}

// log asynchronously with Node's setImmediate()
function log_setImmediate(){
    setImmediate(() => console.log('setImmediate'))
}

This is the order how I called the functions:

log_setImmediate();
log_setTimeout(1);
log_setTimeout(0);
log_loopDelay();
log();

This is the log:

loopDelay
log
setTimeout(0s)
setTimeout(1s)
setImmediate

This result is absolutely counter-intuitive. So me and the console had a very long conversation about this, with tons of different scenarios, varying the order of function calls, varying the delays, logging again and again, until I saw some pattern. Doesn't mean I understand it, I can only describe it.

For instance, setTimeout(0) always takes precedence over setImmediate() - or, more generally speaking - whenever a (finished) timer and setImmediate() are in the thread pool at the same time, the event loop will always pick up the timer first.

In the above example, all three async calls are in the thread pool, before the long while loop runs. By the time that loop has finished, both timers have run out, and they log in order. setImmediate() comes last.


Changing the order of the function calls:

log_setImmediate();
log_setTimeout(1);
log_loopDelay();
log();
log_setTimeout(0);

When the while loop finishes, setImmediate() and setTimeout(1) are both sitting in the queue, and timers come first. Then, setTimeout(0) appears. So the situation for the event loop is: I have two timers here that I should push back onto the call stack in their order, and finally one call to setImmediate(). The log:

loopDelay
log
setTimeout(1s)
setTimeout(0s)
setImmediate

I'm beginning to get the hang of it. Commenting out the long loop and applying the rules I've found:

log_setImmediate();
log_setTimeout(1);
log();
log_setTimeout(0);

All three async calls pretty much arrive in the thread pool at the same time. setTimeout(1) will certainly come last, and setTimeout(0) will be first, followed by setImmediate():

log
setTimeout(0s)
setImmediate
setTimeout(1s)

Such a relief when things makes sense again. By the way, instead of playing detective in the console, I could've just done my research first. Here's an article that gives a very detailed overview on how the event loop handles async callbacks in the callback queue:

nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

It basically confirms what I've found out through the tedious process of endless logging - that callbacks from setTimeout() are picked up by the loop before a callback from setImmediate(). Additionally the article also explains why.


✏ Phases of the Event Loop

In short, saying that the callback queue works according to FIFO is oversimplified. It also depends on what is waiting in the queue. The event loop goes through different phases (image from the link above):

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

setImmediate callbacks will be invoked during the "check" phase, that's why they always come after callbacks from the "timers" phase.


✏ Introducing process.nextTick()

There's one last thing about the event loop in Node.js that I'd like to explore. I won't go into this in depth, because the article I've linked above gives a far better explanation than I ever could at this point - but if you really want to make sure that an async piece of code gets executed first, you'll have to use process.nextTick() instead of setImmediate().

While setImmediate() guarantees that the callback will be executed during the next tick of the event loop, process.nextTick()will fire during the current tick, as soon as the current phase of the loop transitions to the next phase, or right before the next tick. process.nextTick() will be squeezed within phases, and it can block the event loop if it's called recursively. This won't happen with recursive calls of setImmediate(), because each call will be executed only on the next tick.

The confusing names of setImmediate() and process.nextTick() are also a good example to illustrate why you should spend some time thinking about the names of your variables and functions, especially if you're writing software that is used by many people. Doing so will prevent you from being the target of endless banter in various articles and conference talks.


✏ Recap

I've learned

  • the event loop doesn't push functions back on the call stack strictly in order, but goes through phases, during which only certain types of callback functions get pushed back
  • process.nextTick() will fire immediately
  • setImmediate() will fire on the next tick

✏ Resources

The Node.js Event Loop, Timers and process.nextTick()

setImmediate vs process.nextTick in NodeJs


✏ Next:

  • asynchronous programming with callbacks, promises or async/await

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋


✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()