Scheduling Execution in Node.js
Differences between setTimeout, setImmediate, and process.nextTick.
A commonly misunderstood concept in Node.js is the difference between
process.nextTick. While all three functions are used to schedule the execution of a function, it is important to understand when Node.js will execute them.
Consider the following example:
setImmediate(() => console.log('Set Immediate')); setTimeout(() => console.log('Set Timeout'), 0); process.nextTick(() => console.log('Process NextTick'));
What do you think the order of output to
stdout would be?
The answer may surprise you:
> node index.js Process NextTick Set Timeout Set Immediate
The body of the script index.js will be executed synchronously as one block, scheduling callbacks to be executed later. The reason why I mention that is because none of the calls to
process.nextTick will have their callbacks immediately executed within the same block as the main script.
Understanding execution order in the event loop
To understand why the functions execute in different orders, let's review the order of the Node.js event loop:
Screen capture of the Node.js Docs on "Event Loop, Timers, and process.nextTick".
setTimeout - is scheduled in the timers phase for execution, though the execution will actually occur at the beginning of the poll phase.
setImmediate - executes in the check phase when the poll phase becomes idle. The poll phase is where 99% of the callbacks in Node.js are executed. This means that
setImmediate is delayed until the event queue is empty.
process.nextTick - callback is enqueued in something called the
nextTickQueue. This queue is drained (and executed) right after the current block finishes. When the queue is empty, control is returned back to the event loop.
Using this knowledge to understand the previous example, we can follow the order of operations in the event loop:
- Entry script (index.js) code block executes.
process.nextTickhad enqueued a callback, so that callback is executed.
- Event loop resumes.
- The timers phase detects a timer with a delay of 0ms has lapsed (from
setTimeout). The callback is enqueued.
- The event loop enters the poll phase. The timer callback is dequeued and executed.
- The poll queue is empty and there are
setImmediatecallbacks scheduled; the event loop moves to the check phase.
setImmediatecallback is executed.
- Event loop restarts. There are no timers or I/O operations pending. The process exits.
When should I use each scheduling function?
setTimeout - use this to schedule a function to be executed after a delay of more than zero milliseconds. If you need the function to be immediately executed after the active code block, use
process.nextTick. If you don't need code to be immediately executed (allowing other callbacks to be invoked), use
setImmediate - this is the preferred approach. Node.js recommends you use this instead of
process.nextTick for all occasions!!!! This approach ensures the fair execution of code within the process, particularly I/O callbacks that will be waiting for the
nextTickQueue to drain before they can be executed.
process.nextTick - ideal for situations when you want to ensure callbacks passed to a function are handled asynchronously, or you want to immediately return control of the thread to the caller, but do not want to wait for other callbacks to execute before the supplied callback. I don't know in what use cases this may be preferred to
setImmediate, but it's an approach available to you.
Finally, it is important to reiterate Node's warning about the use of
process.nextTick. If you enqueue a large number of callbacks in the
nextTickQueue, you can potentially starve the event loop by ensuring the poll phase is never reached. This is another reason why you should generally prefer
It's extremely important to understand how the Node.js event loop works. As we saw in the example, it's possible to schedule functions expecting a certain order only to be surprised by what actually happens. The key takeaway is that 99% of the time you should be using
setImmediate instead of
setTimeout(fn, 0) since both functions compete against waiting I/O callbacks and potentially starve the event loop.
The Node.js documentation is fantastic on this topic. Please read an in-depth description of how the event loop works here: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
You might also be interested in these articles...
Stumbling my way through the great wastelands of enterprise software development.