Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 18

The Event Loop and Microtasks

JavaScript executes one thing at a time. The event loop is the scheduler that decides what runs next, and microtasks are the priority lane it consults before anything else. This chapter assumes you are comfortable with promises, callback-based async APIs, and lexical scope.
Level: AdvancedLength: In depth3 min read

Read at another depth

The Event Loop and Microtasks

JavaScript executes one thing at a time. The event loop is the scheduler that decides what runs next, and microtasks are the priority lane it consults before anything else. This chapter assumes you are comfortable with promises, callback-based async APIs, and lexical scope.

The mental model

The runtime maintains at least these queues and stacks:

  • Call stack: the currently executing synchronous frames. Only one exists; nothing runs until it empties.
  • Task queue (macrotasks): callbacks from timers (setTimeout, setInterval), I/O completion, UI events, and message channel posts.
  • Microtask queue: promise reaction jobs, queueMicrotask, MutationObserver callbacks, and in Node, process.nextTick (which has its own higher-priority queue).

The loop iterates roughly as follows:

  1. Run all synchronous code until the call stack is empty.
  2. Drain the microtask queue completely — including microtasks queued by microtasks.
  3. If the renderer is doing updates, it may paint here (browsers only).
  4. Take one task from the task queue, run it to completion.
  5. Drain microtasks again. Repeat.

The consequence that matters: microtasks always interleave before the next macrotask and before rendering. A promise resolved inside a timer callback still runs its handlers before the next timer fires, and before the browser paints.

Observing the ordering

Code example in javascript
console.log('1: sync');

setTimeout(() => console.log('4: timer'), 0);

Promise.resolve().then(() => {
  console.log('3: microtask');
});

console.log('2: sync');

// Output: 1: sync, 2: sync, 3: microtask, 4: timer

setTimeout(fn, 0) does not mean "run in zero milliseconds." It means "enqueue as a task, but not before the delay, and never while the stack or microtask queue is busy." A setTimeout(..., 0) can execute well after 0 ms if the loop is backed up.

Microtask exhaustion and starvation

Because the microtask queue is drained completely, a microtask that queues another microtask can starve everything: rendering, timers, input. This never happens with tasks, since the loop takes only one task per turn.

Code example in javascript
function spin() {
  queueMicrotask(spin); // infinite loop, but only at microtask priority
}
spin();
// Page freezes: timers never fire, paint never happens.

This is a real failure mode in browsers: an unbounded promise chain (each .then scheduling another) blocks paint and user interaction while never blocking a single thread "officially."

Node: nextTick, microtasks, and phases

Node runs libuv's event loop with phases: timers, pending callbacks, poll, check (setImmediate), and close callbacks. Two additions complicate ordering:

  • process.nextTick callbacks run before promise microtasks, in a queue with a default cap of 1000 per turn (--max-stack-size and nextTick limits are separate; the 1000-drain rule prevents nextTick starvation of I/O).
  • Microtasks are drained between each phase transition and after each timer callback, not only after the entire loop turn.
Code example in javascript
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
// Order between these two is NOT guaranteed inside the main module
// (libuv timing decides), but is deterministic inside an I/O callback:
const fs = require('fs');
fs.readFile(__filename, () => {
  // inside the poll phase: setImmediate always wins over setTimeout
  setImmediate(() => console.log('immediate'));
  setTimeout(() => console.log('timeout'), 0);
});

Gotchas

await is a microtask boundary. Each await suspends the async function and schedules its continuation as a microtask. But the modern spec interleaves await continuations differently from raw .then chains in edge cases — an await schedules with roughly one extra tick in older engines. Do not build logic that depends on tick counts; depend on ordering guarantees only.

Microtasks cannot abort a running task. If a synchronous loop runs for five seconds, no promise handler, no timer, no UI event runs during it. Long-running synchronous work must be chunked — either across setTimeout/setImmediate turns or via scheduler.yield() where available.

Unhandled rejection timing. Rejections are reported only after the microtask queue drains, so a try/catch added in a later tick is still in time — and a catch handler attached much later can rescue a rejection the console already flagged.

Timers in background tabs. Browsers clamp timer resolution (typically to 1s or worse for hidden tabs) and may throttle heavily. Never use setTimeout for precision; use it for scheduling.

Decision criteria: task vs microtask

NeedUse
Yield to rendering/input between steps`setTimeout`, `requestAnimationFrame`, `scheduler.postTask`
Run soon, atomically after current work`queueMicrotask`, resolved promise
Node: run before promise reactions`process.nextTick`
Node: run on next loop iteration, after I/O`setImmediate`

exercise

Exercise

What does this print, and why? console.log('a'); setTimeout(() => console.log('b'), 0); Promise.resolve().then(() => { console.log('c'); setTimeout(() => console.log('d'), 0); }); console.log('e');

Show answer

a, e, c, b, d. Sync logs 'a' and 'e' first. Then the microtask queue drains: 'c' runs and enqueues the 'd' timer as a task. The loop then takes one task: 'b'. On the next turn, 'd' fires.

Exercise

Why does replacing queueMicrotask(spin) with setTimeout(spin, 0) in the starvation example un-freeze the page?

Show answer

The loop takes only one task per turn, so each setTimeout turn lets rendering and other tasks proceed. Microtasks, by contrast, are drained fully before the loop advances, so self-perpetuating microtasks never yield.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: The Event Loop and Microtasks

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that