Non‑deterministic execution order of setImmediate() and setTimeout(0) in top‑level Node.js scripts
0 reputation · 09 Apr 2023, 01:20 UTC
0 reputation · 09 Apr 2023, 01:20 UTC
Determine whether Node.js guarantees that setImmediate() callbacks run before setTimeout(fn,0) when both are scheduled from the synchronous top‑level scope, outside any I/O phase.
The event‑loop specification defines the check and timer phases but does not fix their relative priority for callbacks queued in the main module, and recursive process.nextTick() calls can prevent the loop from advancing to the check phase, making the outcome dependent on implementation details.
What is the defined execution order of setImmediate() versus setTimeout(0) in a non‑I/O context? Does recursive process.nextTick() block the event loop from reaching the check phase? Which ordering pattern emerges when both mechanisms are used in the same tick?
Node.js does not guarantee that setImmediate() callbacks run before setTimeout(fn, 0) when both are scheduled from the synchronous top-level scope. The execution order in this specific context is non-deterministic.
The non-deterministic behavior stems from how the event loop initializes and the precision of system timers:
setTimeout(fn, 0) is scheduled in the timers phase.setImmediate() is scheduled in the check phase.When the script starts, the event loop enters the timers phase. If the process is fast enough that the timer has not yet "expired" (even with a 0ms delay, which is often coerced to 1ms internally), the loop skips the timer callback and proceeds through the poll phase to the check phase, executing setImmediate() first. If the timer expires before the loop reaches that phase, setTimeout() executes first.
process.nextTick() is not a phase of the event loop; it is part of the nextTickQueue, which is processed immediately after the current operation completes, regardless of the current phase. Because the queue must be exhausted before the loop can advance, a recursive process.nextTick() call will effectively starve the event loop, blocking it from ever reaching either the timers or check phases.
To observe this behavior, run the following snippet multiple times:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
In top-level execution, you will see the order flip between runs. However, if you wrap these calls inside an I/O callback (e.g., fs.readFile), setImmediate() will always run first because the check phase immediately follows the poll phase where I/O callbacks are executed.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 09 Apr 2023, 11:07 UTC
From synchronous top-level code, both setImmediate() and setTimeout(fn, 0) are queued before the event loop begins its first iteration. In the standard Node.js loop model, the timer phase runs before the check phase, so setTimeout(0) is typically observed first. However, the specification does not mandate this priority for callbacks queued from the main module, so the order can shift across Node.js releases.
Recursive process.nextTick() calls do not merely delay the check phase; they can starve it entirely. The nextTick queue is drained after the current operation completes, and if each callback re-enqueues another, the loop never advances to the next phase. To verify behavior on your target version, run a minimal script that logs a marker from each callback and compare the output across Node.js 18, 20, and 22.