Skip to content

Step 1 of 7 · Reading · ~3 min

Learn

The Runtime

Event Loop and Microtasks

In JavaScript Intermediate you saw that a promise callback runs before a setTimeout callback. This lesson gives you the whole rule: which queues exist in Node, what order they drain in, and where await fits. By the end you can predict the output of any mix of timers, promises and process.nextTick without running it.

One thread, several queues

JavaScript runs one piece of code at a time, to completion. Work that must wait goes into a queue:

QueueFilled byDrained
nextTick queueprocess.nextTick(fn) (Node only)completely, first
microtask queuepromise.then, await, queueMicrotask(fn)completely, second
timerssetTimeout, setIntervalone expired timer at a time
check queuesetImmediate(fn) (Node only)after I/O callbacks

The script itself counts as the first task. When it ends, and again after every timer or I/O callback, Node drains the nextTick queue, then the microtask queue, and repeats both until both are empty. A callback that schedules more work of the same kind extends the drain; it does not wait its turn behind a timer.

javascript

Sync code prints start and end and schedules three things. The drain runs tick, then promise. Only then does the loop reach the timer.

The order inside a drain

Each callback's own scheduling follows the same rules, which creates ordering you can compute:

javascript

All pending ticks run (t1), which queues a microtask behind m1. The microtask pass then runs to exhaustion: m1, the one from the tick, and m2 that m1 added. The tick that m1 queued waits until that pass is over.

Timers: due time, not a promise of "now"

A timer's callback is due at the moment you called setTimeout plus its delay. Node treats a delay of 0 as 1 ms, so setTimeout(f, 0) and setTimeout(g, 1) run in creation order. When several timers are due, they run in order of due time, ties by creation order, and the nextTick and microtask queues drain after each one:

javascript

A delay is a minimum, not a schedule: a busy script makes every timer late. Which of setTimeout(f, 0) and setImmediate(g) fires first from the main script depends on how fast the process starts, so never rely on it. Inside an I/O callback setImmediate always wins.

await is a microtask

await x pauses the async function and schedules the rest as a microtask once x settles. Everything before the first await runs synchronously inside the call:

javascript

f2 comes before p because f's continuation was queued first.

Starvation

The drain does not stop while work keeps arriving. A callback that reschedules itself with process.nextTick or a promise never lets a timer or an I/O callback run. To give the rest of the program a turn, defer with setImmediate or a timer.

Common mistakes

  • Expecting setTimeout(f, 0) to run before a .then callback scheduled later. Timers always lose to both microtask kinds.
  • Treating process.nextTick as "the next loop iteration". It runs before promise callbacks, in the same drain.
  • Believing await blocks. Only the async function is suspended; the caller continues immediately.

Your exercise

You will write the scheduler core as a small simulator of these rules. The usual slip is a single pass over the queues: a microtask can schedule a tick and a tick can schedule a microtask, so the drain has to repeat until both are empty.

Up nextSymbols and Well-Known SymbolsThe Runtime

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…