Lesson 55 of 80
Microtasks-before-macrotasks holds in both environments — but Node splits macrotasks into ordered phases, and adds a queue that jumps ahead of even microtasks.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The event loop lesson and the microtask/macrotask lesson establish the core rule: microtasks fully drain before the next macrotask runs. That rule is a JavaScript-language-level guarantee and holds everywhere — but "the next macrotask" means something more specific in Node than "whichever one's next" in the browser.
The browser's macrotask queue is roughly one bucket: timers, UI events, and I/O callbacks all compete, and the spec leaves a lot of ordering detail to the browser vendor. Node, running on libuv instead of a browser's event loop implementation, organizes macrotasks into named, ordered phases that always run in the same sequence every single turn: timers → pending callbacks → poll (I/O) → check (setImmediate) → close callbacks. And Node adds one more queue, process.nextTick, that isn't a macrotask or a microtask — it runs before microtasks, after every single callback.
console.log("start");
setTimeout(() => console.log("timeout"), 0); // timers phase
setImmediate(() => console.log("immediate")); // check phase
process.nextTick(() => console.log("nextTick")); // Node-only, jumps the queue
Promise.resolve().then(() => console.log("promise")); // microtask
console.log("end");
// Node output: start, end, nextTick, promise, timeout, immediate
// (timeout vs immediate order can actually flip if this runs at the top of
// the script, rather than inside an I/O callback — see below)
nextTick beats the promise microtask because Node drains process.nextTick's queue completely, then the promise microtask queue, before moving into the timers phase at all. Only once both of those are empty does Node enter the phase machinery — and setTimeout(fn, 0) lands in the timers phase, setImmediate in the check phase, which always runs after timers on a normal script.
process.nextTick doesn't exist in the browser at all. It's a Node API, not part of the JavaScript language or a Web API — porting Node code that leans on it to run in a browser needs a real replacement (usually queueMicrotask, which is close but runs after nextTick would have, not before).nextTick callback still runs before Node moves on. Node drains nextTick and microtasks in a tight alternating loop — a nextTick callback that queues a promise .then() doesn't get deferred to "later," it's still handled before the event loop proceeds to the next phase.setTimeout(fn, 0) calls in a browser are still first-in-first-out relative to each other, but there's no browser equivalent of "timers always run before check callbacks" as a named, guaranteed phase order — that structure is specific to Node's libuv-based loop.Everything the event loop lesson teaches about microtasks draining before macrotasks still holds here without exception — Node hasn't changed the language's rule, it's added phase structure within the macrotask side and one extra Node-specific queue that runs even before microtasks. Anyone who's solid on the microtask-vs-macrotask tracing exercise already has the hard part; this lesson is really about not assuming the browser's flatter macrotask model when reading or writing Node code.