Understanding the Execution Order of setTimeout, setImmediate, and process.nextTick in Node.js
Environment
node: v12.16.2
Node.js provides several APIs for scheduling asynchronous work, including setTimeout, setInterval, setImmediate, and process.nextTick.
They are scheduled in different parts of the event loop, which means their execution order can sometimes be surprising.
This post walks through how these APIs behave and why their ordering is not always obvious.
The Event Loop Phases
To understand the behavior of these APIs, it’s helpful to first look at the simplified structure of the Node.js event loop.
timers
↓
pending callbacks
↓
idle / prepare
↓
poll
↓
check (setImmediate)
↓
close callbacks
Two phases are particularly relevant here:
- timers phase →
setTimeout,setInterval - check phase →
setImmediate
Additionally, process.nextTick runs outside of these phases.
setTimeout vs setInterval
Both setTimeout and setInterval are executed in the timers phase.
Their order depends primarily on the delay value provided.
- Callbacks with shorter delays run first.
- If the delays are the same, the callback that was registered earlier typically executes first.
Example:
const timer = setInterval(() => {
console.log('interval')
clearInterval(timer)
}, 1)
setTimeout(() => {
console.log('timeout')
}, 1);
/* ouput:
* interval
* timeout
*/
setTimeout vs setImmediate
setImmediate callbacks are executed in the check phase, while the callbacks of setTimeout are executed in the timers phase**, looks like setTimeout stands in the first position, right? But in many situations this leads to the following order:
setImmediate → SetImmediate
Look at the following example:
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
After registering two callbacks, the program enters the event loop.
At this moment, if:
- the **timer ** has not yet reached its scheduled time
- the poll queue is empty
- the **setImmediate queue has pending tasks
then the poll phase behaves as follows:
poll phase
↓
detects setImmediate callbacks exist
↓
skips waiting and go directly to the check phase
↓
executes setImmediate callbacks
Then, in the next iteration of the event loop:
timers phase
↓
executes timeout callback
So the final execution order becomes:
immediate
timeout
However, things become different when adding the following code.
Consider this example:
setTimeout(() => {
console.log('timeout')
})
const start = process.hrtime.bigint();
while (process.hrtime.bigint() - start < 1_000_000n) {}
setImmediate(() => {
console.log('immediate')
})
Output
timeout
immediate
Why does this happen?
Because this piece of synchronous blocking code gives setTimeout enough time to reach the timer ready state, so in this loop, the sequence will be:
timers phase
↓
timeout
↓
poll
↓
check
↓
immediate
But This kind of busy waiting is generally not recommended in real-world projects, because it blocks the event loop and fully occupies a CPU core. If you want to guarantee the execution order, use direct nesting:
setTimeout(() => {
console.log('timeout')
setImmediate(() => {
console.log('immediate')
})
}, 0)
process.nextTick
process.nextTick() behaves differently from the other APIs.
Although it is asynchronous, it does not belong to any event loop phase.
Instead, Node.js maintains a nextTick queue, which is processed immediately after the current operation finishes, before the event loop continues.
This means process.nextTick() always runs before timers and immediates.
Example:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B() {
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT');
});
Output:
1
2
TIMEOUT
The entire nextTick queue is drained before the event loop proceeds to the timers phase.
Practical Takeaways
A few practical guidelines:
- Use
process.nextTickwhen work must run immediately after the current operation. - Use
setImmediatewhen you want to schedule work after I/O callbacks. - Use
setTimeoutwhen you need actual time-based scheduling.
In practice, most applications do not rely on the exact ordering between setTimeout(0) and setImmediate. However, when debugging asynchronous behavior or writing low-level libraries, these distinctions become important.
Comments