codejump
MediumPro challengeJavaScriptTypeScript

Node.js — Event Loop Order

Node.jsBackendEvent LoopAsync

"What does this print?" with process.nextTick, promises, setTimeout and setImmediate is
the most common Node interview question — and knowing the answer explains real bugs, like a
callback that runs "too early". You will both run code on Node's queues and predict the
order without running it.

A program is a list of steps { log, via, then? }. via says how the step is scheduled;
when it runs, it logs log, then schedules its then steps (if any) in order.

Part 1 — run(steps, log)

Schedule each step with the real API, so that it does log.push(step.log) then
run(step.then ?? [], log):

viaAPI
'sync'run right away
'nextTick'process.nextTick(fn)
'promise'Promise.resolve().then(fn)
'timeout'setTimeout(fn, 0)
'immediate'setImmediate(fn)

The driver calls run from inside an I/O callback (fs.readFile's), the way code inside
a request handler runs.

Part 2 — predict(steps)

Return the order of the logs without scheduling anything, by simulating four queues:

1. Executing a step logs it, then *enqueues* its children: a sync child executes immediately,
others go to the back of their queue.
2. After every callback (the initial I/O callback included), Node drains: every
nextTick (ticks added meanwhile included), then every promise callback (new ones
included); if that added ticks, start over.
3. Then the loop alternates phases, starting with the check phase because we come from an
I/O callback: run the immediate steps queued *before the phase started*, draining after
each one; then the timers phase does the same with timeout steps; then check, timers...
until both queues are empty.

Example

[{ log: 'timeout', via: 'timeout' }, { log: 'immediate', via: 'immediate' },
 { log: 'promise', via: 'promise' }, { log: 'tick', via: 'nextTick' }, { log: 'sync', via: 'sync' }]
// → ['sync', 'tick', 'promise', 'immediate', 'timeout']

The driver returns { predicted, actual }: both must equal Node's real order.

Sample tests

Test #1One of each
Input: [[{"log":"timeout","via":"timeout"},{"log":"immediate","via":"immediate"},{"log":"promise","via":"promise"},{"log":"tick","via":"nextTick"},{"log":"sync","via":"sync"}]]
Output: {"actual":["sync","tick","promise","immediate","timeout"],"predicted":["sync","tick","promise","immediate","timeout"]}
Test #2Ticks before promises
Input: [[{"log":"A","via":"promise"},{"log":"B","via":"nextTick"},{"log":"C","via":"promise"},{"log":"D","via":"nextTick"}]]
Output: {"actual":["B","D","A","C"],"predicted":["B","D","A","C"]}
Test #3A tick added by a promise waits for the promise queue
Input: [[{"log":"p1","via":"promise","then":[{"log":"t1","via":"nextTick"}]},{"log":"p2","via":"promise"}]]
Output: {"actual":["p1","p2","t1"],"predicted":["p1","p2","t1"]}
Test #4A timer schedules the next round
Input: [[{"log":"T1","via":"timeout","then":[{"log":"T2","via":"timeout"},{"log":"I1","via":"immediate"},{"log":"P1","via":"promise"}]}]]
Output: {"actual":["T1","P1","I1","T2"],"predicted":["T1","P1","I1","T2"]}