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 16

The Event Loop and Microtasks

Prerequisites assumed: you know promises and their states, how `setTimeout` schedules work, closures, and the difference between synchronous and asynchronous functions.
Level: AdvancedLength: Mid2 min read

Read at another depth

The Event Loop and Microtasks (and what async/await compiles down to)

Prerequisites assumed: you know promises and their states, how setTimeout schedules work, closures, and the difference between synchronous and asynchronous functions.

The execution model in one paragraph

JavaScript runs your code on a single thread, one call stack at a time. The event loop is the scheduler that keeps pulling work onto that stack. It is not part of the language itself; ES2015 specified jobs (microtasks), and hosts like browsers and Node supply the loop that drains them. Understanding the loop's drain order is the difference between guessing at async bugs and predicting them.

Tasks vs microtasks

The loop maintains at least two queues:

  • Task (macrotask) queue: timers, I/O callbacks, UI events, setImmediate in Node. The loop takes one task per iteration.
  • Microtask queue: promise reactions, queueMicrotask, MutationObserver, await continuations. After each task — and between any two tasks — the loop drains the microtask queue until it is empty, including microtasks spawned by microtasks.

This is why a microtask scheduled now always runs before the next timer, no matter how long ago the timer was set.

Code example in javascript
console.log('1: sync');
setTimeout(() => console.log('4: task'), 0);
Promise.resolve().then(() => {
  console.log('3: microtask');
  Promise.resolve().then(() => console.log('3b: nested microtask'));
});
console.log('2: sync');
// 1: sync, 2: sync, 3: microtask, 3b: nested microtask, 4: task

Note the nested .then: microtask drainage is not a snapshot; newly queued microtasks join the current drain. An infinitely chaining promise starves timers and rendering permanently.

caption: Event loop ordering: sync code, then full microtask drain, then one task, repeat. alt: Sequence diagram showing synchronous code running first, the microtask queue draining fully including chained microtasks, then a single task from the task queue, repeating.

Where async/await lands

async/await is promise syntax, not a separate mechanism. An async function:

  1. Runs synchronously until the first await.
  2. At await value, wraps value in a promise, registers the rest of the function as a reaction (a microtask), and returns.
  3. When the awaited promise settles, the continuation runs as a microtask.

So await pauses that function, never the thread. Code after the first await is effectively .then — meaning it is asynchronous relative to the caller, even for already-resolved values.

Code example in javascript
async function main() {
  console.log('a');
  const v = await Promise.resolve(42);
  console.log('c', v); // microtask continuation
}
console.log('start');
main();
console.log('b');
// start, a, b, c 42

Gotchas

  • Zero-delay await: await on an already-settled promise still yields one microtask tick. Values that are not thenables (plain numbers in older engines) may cost an extra tick — observable in shared-queue ordering, not in correctness.
  • Awaits in loops are sequential: for...of with await serializes; use Promise.all for concurrency when results are independent.
  • Unawaited async calls: errors become unhandled rejections; nothing enforces that callers await.
  • Node vs browser: Node layers phases (timers, poll, check) on top of this model; process.nextTick runs before promise microtasks. Keep drains-order reasoning, but don't assume setTimeout(fn, 0) and setImmediate interleave identically across hosts.

Decision criteria

Use plain promises when composing with combinators (all, allSettled, race, any); use async/await for linear, error-sensitive flows where try/catch reads naturally. Never rely on microtask ordering between unrelated subsystems — treat it as an ordering guarantee within one function's flow, not a scheduling API.

Exercise

In what order do Promise.resolve().then(f), setTimeout(g, 0), and a synchronous h() run?

Show answer

h() first (current stack), then f (microtask drained before the next task), then g (task queue, one tick later).

Exercise

Does await Promise.resolve() execute the continuation synchronously?

Show answer

No. It queues the continuation as a microtask, so the caller and any synchronous code after the call run first.

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