Skip to content
  • The Event Loop · JavaScript

The Event Loop

LearnViaNotes @learnvianotes· 3 min read

One thread, one call stack — and a loop that, whenever the stack is empty, drains all microtasks and then takes the next macrotask. Predicting output order is a top interview skill.

The moving parts

One turn of the loop

  1. Run a task

    Take the oldest macrotask (the initial script is the first) and run it to completion.

  2. Drain microtasks

    Run every microtask — including ones queued while draining — until the queue is empty.

  3. Render (browsers)

    If it's time for a frame: requestAnimationFrame callbacks, style, layout, paint.

  4. Repeat

    Pick the next macrotask.

Predict the output

JavaScript
console.log("1: sync start");
setTimeout(() => console.log("6: timeout (macrotask)"), 0);
Promise.resolve()
  .then(() => console.log("3: promise then (microtask)"))
  .then(() => console.log("5: chained then (microtask)"));
queueMicrotask(() => console.log("4: queueMicrotask"));
console.log("2: sync end");
Output
1: sync start
2: sync end
3: promise then (microtask)
4: queueMicrotask
5: chained then (microtask)
6: timeout (macrotask)

async/await in the ordering

JavaScript
async function task() {
  console.log("B: task starts (sync part)");
  await null;                               // everything after this is a microtask
  console.log("E: task resumes after await");
}
console.log("A: script start");
setTimeout(() => console.log("G: timeout"), 0);
task();
new Promise((resolve) => {
  console.log("C: executor runs synchronously");
  resolve();
}).then(() => console.log("F: then"));
console.log("D: script end");
Output
A: script start
B: task starts (sync part)
C: executor runs synchronously
D: script end
E: task resumes after await
F: then
G: timeout

Microtasks can starve the loop

JavaScript
let count = 0;
setTimeout(() => console.log("timeout finally runs, microtasks ran:", count), 0);
function spin() {
  if (++count < 1000) queueMicrotask(spin);   // keeps refilling the queue
}
queueMicrotask(spin);
Output
timeout finally runs, microtasks ran: 1000

Node specifics: nextTick, setImmediate

Node phase / queueRuns
process.nextTick queueAfter the current operation, before promise microtasks (from CommonJS/callbacks)
Promise microtasksRight after the nextTick queue
timersExpired setTimeout / setInterval
pollI/O callbacks (fs, network)
checksetImmediate
close'close' events
JavaScript
setTimeout(() => {
  console.log("inside a timer callback:");
  setTimeout(() => console.log("  4. setTimeout 0 (next timers phase)"), 0);
  setImmediate(() => console.log("  3. setImmediate (check phase comes first)"));
  Promise.resolve().then(() => console.log("  2. promise microtask"));
  process.nextTick(() => console.log("  1. process.nextTick"));
}, 0);
Output
inside a timer callback:
  1. process.nextTick
  2. promise microtask
  3. setImmediate (check phase comes first)
  4. setTimeout 0 (next timers phase)

What is the difference between microtasks and macrotasks?

If JavaScript is single-threaded, how does it do async work?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report