The earlier Event Loop lesson covered the general model — the call stack, the task queue. Node adds two more specific scheduling tools on top of that model, both meaning roughly "run this very soon," but not identically.
A callback passed to process.nextTick runs immediately after the current operation finishes, before the event loop continues to anything else — even before promise callbacks.
console.log('1')
process.nextTick(() => console.log('2 — nextTick'))
Promise.resolve().then(() => console.log('3 — promise'))
console.log('4')
// Output: 1, 4, 2, 3 — nextTick runs before the promise microtaskScheduled to run in a specific later phase of the event loop, after I/O callbacks (like a completed file read) for that cycle have run.
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
// Inside an I/O callback, setImmediate consistently runs before
// a 0ms setTimeout — outside one, the order isn't guaranteed| Tool | When it runs |
|---|---|
| process.nextTick() | Immediately after the current operation — before anything else, including promises |
| Promise .then() / microtask | After nextTick callbacks, still before the event loop moves on |
| setImmediate() | In the "check" phase of the event loop, typically after I/O callbacks |
| setTimeout(fn, 0) | In the timers phase — similar timing to setImmediate, but not guaranteed to be first |
A real footgun with process.nextTick
Calling process.nextTick recursively without a stopping condition starves the entire event loop — nothing else (not I/O, not timers) ever gets a turn, since nextTick callbacks are drained completely before anything else runs.
process.nextTick: ensuring a callback runs after the current synchronous code finishes, but strictly before any I/O or timer — used carefully, and rarely needed in typical application code.setImmediate: deferring work until after pending I/O has been handled, so it doesn't delay something more time-sensitive.