The existing piece on JavaScript's single-threaded async model in this category covers the general idea: one thread, a call stack, and a queue of callbacks waiting their turn. Node.js's event loop, built on a C library called libuv, implements that general idea with a specific, ordered set of phases — and which phase a callback lands in changes exactly when it actually runs.

Node's event loop cycles through a fixed set of phases every pass: timers (where setTimeout and setInterval callbacks whose time has elapsed run), pending callbacks, poll (where most I/O callbacks — a finished file read, an incoming network request — actually fire), check (where setImmediate callbacks run), and close callbacks. Each phase processes its own queue before the loop moves to the next phase, in that fixed order, every single cycle.

Two special queues cut across all of that: process.nextTick and Promise callbacks (microtasks) don't wait for their designated phase — Node drains both of those queues completely in between every other phase, right after the currently executing operation finishes, before moving on to whatever phase comes next. That's why a Promise's .then() callback, or a process.nextTick() call, reliably runs before a setTimeout(fn, 0) scheduled at the same moment, even though intuitively they might seem to fire in the order they were written.

This phase structure is also why setTimeout(fn, 0) and setImmediate() have a genuinely different, sometimes confusing relationship: called from within regular synchronous code, their relative order isn't guaranteed, since it depends on how much time has already elapsed before the timers phase is checked. Called from inside an I/O callback specifically, though, setImmediate() is always guaranteed to run before a same-tick setTimeout, because the check phase (where setImmediate lives) comes right after the poll phase (where the I/O callback itself just ran), ahead of the next pass through timers.

None of this needs to be memorized to write working Node.js code most of the time — but when a bug depends on exact callback ordering between timers, I/O, and promises, knowing that Node processes fixed phases in a fixed order, with microtasks draining fully between each one, is usually the fastest way to reason about why one callback ran before another instead of guessing.