Watch it run
Pick a snippet, then play it or step through. Every step says what just happened.
Promise.resolve().then(() => { console.log("promise 1"); process.nextTick(() => console.log("tick from promise"));});process.nextTick(() => { console.log("tick 1"); Promise.resolve().then(() => console.log("promise from tick"));});Promise.resolve().then(() => console.log("promise 2"));setTimeout(() => console.log("timeout"), 0);console.log("sync");Call stack
What is running right now
Timers
setTimeout callbacks waiting to be due
nextTick queue
Node's own queue: drained first
Microtask queue
V8's queue: promise callbacks
Check (setImmediate)
Runs once per loop turn, after polling for I/O
Console0 lines
Nothing has run yet. Node runs a CommonJS file as one ordinary function call.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.
CommonJS or ES module
The first two snippets are the same code. A CommonJS file (.cjs, or .js without "type": "module") returns to Node when it ends, and Node drains nextTicks before promises. An ES module's top-level code runs from inside a promise job, so the promise queue is already being drained when it ends, and the nextTick queue only gets its turn after that. Code that depends on the exact order of these two queues is fragile.
What it is for
The classic use is letting a caller finish setting up before you call back. An object that emits an event from its constructor would emit before anyone could attach a listener; deferring with process.nextTick fixes that while still running before any I/O.
import { EventEmitter } from "node:events" class Job extends EventEmitter { constructor() { super() process.nextTick(() => this.emit("ready")) // listeners attached below still hear it }}new Job().on("ready", () => console.log("ready"))The danger
The nextTick queue is drained completely, including callbacks added while draining. A function that keeps rescheduling itself with process.nextTick stops the loop from ever reaching the poll phase, so no I/O completes and no timer fires. The same is true for a microtask that keeps queueing microtasks. For new code, prefer queueMicrotask, and use setImmediate when you want to yield to I/O between pieces of work.