DMarlon Studio

Lesson 14 · 14 of 20

The event loop · JavaScript

Understand the order in which Node runs JavaScript: synchronous code, microtasks (promises, nextTick) and macrotasks (setTimeout).

JavaScript · The event loop in DMarlon Studio: the example's code and its output in the terminal

JavaScript runs your code on a single thread, and the event loop decides what comes next. The rule is: first all the synchronous code; then every pending microtask; then one macrotask (such as a timer), then all microtasks again, and so on. The numbers in the output show that order, which is not the order of the file.

Synchronous code goes first: lines 1, 2 and 3. Calling checkout() runs its body straight away up to the first await, which is why 2 comes before 3. The two setTimeout(..., 0) calls do not run right now: they are queued as timers.

When the script ends, Node drains the microtasks. process.nextTick has its own queue, which in Node runs even before promises (4). Then, in the order they were queued, the then (5), queueMicrotask (6) and the rest of checkout after its await (7): resuming an async function is a microtask too.

Only then do the timers run. The microtask created inside timeout A (9) runs before timeout B (10), because microtasks are drained after every macrotask. The practical takeaway: a then always runs before a setTimeout(..., 0), and an endless chain of microtasks would keep timers waiting forever.

Code · main.js

// The numbers show the order Node guarantees, not the order in the file.console.log("1. script starts (synchronous code)");setTimeout(() => {  console.log("8. timeout A (macrotask)");  Promise.resolve().then(() => console.log("9. microtask created by timeout A"));}, 0);setTimeout(() => console.log("10. timeout B (macrotask)"), 0);Promise.resolve().then(() => console.log("5. promise.then (microtask)"));queueMicrotask(() => console.log("6. queueMicrotask (microtask)"));process.nextTick(() => console.log("4. process.nextTick (before promises)"));async function checkout() {  console.log("2. checkout runs synchronously until its first await");  await null;  console.log("7. checkout resumes after await (microtask)");}checkout();console.log("3. script ends: now the microtasks, then the timers");

Output · node main.js

1. script starts (synchronous code)
2. checkout runs synchronously until its first await
3. script ends: now the microtasks, then the timers
4. process.nextTick (before promises)
5. promise.then (microtask)
6. queueMicrotask (microtask)
7. checkout resumes after await (microtask)
8. timeout A (macrotask)
9. microtask created by timeout A
10. timeout B (macrotask)

Key points

  • Synchronous code first, then microtasks, then macrotasks.
  • Promises, queueMicrotask and resuming after await are microtasks.
  • setTimeout creates macrotasks, even with a delay of 0.
  • In Node, process.nextTick runs before promises.
  • All microtasks are drained after each macrotask.

Exercise

Add a 0 ms setTimeout inside the then on line 5, predict where its message will appear before running the program, and then check.