JavaScript: the event loop as a cooperative scheduler

Predict the order in which synchronous code, nextTick, promises, and timers run in Node.js.

JavaScript
console.log("script start");

setTimeout(() => console.log("timeout (macrotask)"), 0);
Promise.resolve().then(() => console.log("promise (microtask)"));
process.nextTick(() => console.log("nextTick"));

console.log("script end");
Output
script start
script end
nextTick
promise (microtask)
timeout (macrotask)

Explanation

JavaScript runs your code on one thread. Instead of preempting a running task, the runtime uses cooperative scheduling: a task runs to completion, and only then does the event loop pick the next one. Long synchronous work therefore blocks everything else.

In Node.js, after the current script finishes, the process.nextTick queue runs first, then promise callbacks (the microtask queue), and only then timers and I/O callbacks, which are macrotasks. A timer of 0 ms still waits for the current script and all microtasks. Knowing this order explains many surprising outputs.

Explore the dry run
Guided walkthroughReady
1console.log("script start");
2
3setTimeout(() => console.log("timeout (macrotask)"), 0);
4Promise.resolve().then(() => console.log("promise (microtask)"));
5process.nextTick(() => console.log("nextTick"));
6
7console.log("script end");

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Synchronous code runs first, in order.—
script start
The callback is queued for a later turn of the event loop.—
script start
The callback goes on the microtask queue.—
script start
The script finishes before any queued callback runs.—
script start
script end
After the script ends, nextTick runs first, then the microtask, then the timer.—
script start
script end
nextTick
promise (microtask)
timeout (macrotask)

Cost and common mistakes

Each callback runs in O(1) scheduling time plus its own work.

  • A long synchronous loop, or a CPU-heavy function, blocks timers, I/O, and every other callback.
  • Endlessly scheduling nextTick or promise callbacks can starve timers and I/O because the microtask queues are drained completely first.
  • The exact order of setTimeout(0) and setImmediate differs depending on context. Do not rely on it.

Try it yourself

Where would a setImmediate callback run in this order?

Reveal answer and reasoning

After the microtasks, in the check phase of the event loop. Relative to a 0 ms timer the order can vary when called from the main module.

Further reading