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:
| Queue | Filled by | Drained |
|---|---|---|
| nextTick queue | process.nextTick(fn) (Node only) | completely, first |
| microtask queue | promise.then, await, queueMicrotask(fn) | completely, second |
| timers | setTimeout, setInterval | one expired timer at a time |
| check queue | setImmediate(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.
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:
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:
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:
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.thencallback scheduled later. Timers always lose to both microtask kinds. - Treating
process.nextTickas "the next loop iteration". It runs before promise callbacks, in the same drain. - Believing
awaitblocks. 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.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…