Compile Ready
Module 1 · JavaScript Fundamentals

The Event Loop (Introduction)

Intermediate9m read5m practice14m total
Event LoopMicrotasksMacrotasksAsync

Introduction

The event loop is the mechanism that lets single-threaded JavaScript be asynchronous and non-blocking. It continuously checks: is the call stack empty? If so, what callback should run next?

This is a first, practical introduction — Module 8 revisits it in full depth. Master the ordering rules here and most "what's the output?" questions become easy.

Why This Matters

The event loop is the most-asked JavaScript interview topic. Being able to trace macrotasks vs microtasks out loud, correctly, instantly signals competence. It also underpins real debugging: race conditions, UI jank, and unexpected ordering.

Theory

The core loop

The event loop repeats these steps forever:

  1. If the call stack is not empty, keep running (synchronous code has priority).
  2. When the stack is empty, run every queued microtask (draining the queue, including microtasks scheduled by microtasks).
  3. Run one macrotask (a.k.a. task) from the task queue.
  4. Go back to step 2 (drain microtasks again).
  5. In the browser, render between iterations if needed.

Macrotasks vs microtasks

Macrotasks (tasks)Microtasks
ExamplessetTimeout, setInterval, I/O, UI events, setImmediate (Node)Promise.then/catch/finally, queueMicrotask, await continuation, MutationObserver
When they runone per loop iterationall of them, after each task
Prioritylowerhigher

The golden rules

  1. Synchronous code always runs first, to completion.
  2. All microtasks run before the next macrotask.
  3. setTimeout(fn, 0) does not run immediately — it queues a macrotask.
  4. await x pauses the async function and schedules the rest as a microtask.

Watch out: microtask starvation

Because the loop drains all microtasks before the next task, a microtask that keeps scheduling more microtasks can starve timers and rendering forever. Prefer not to recurse infinitely in microtasks.

Visual Diagrams

One turn of the event loop
call stack empty?
      |
      v
[ drain ALL microtasks ]  <-- .then, queueMicrotask, await continuations
      |
      v
[ run ONE macrotask ]     <-- setTimeout, I/O, UI event
      |
      v
[ drain ALL microtasks ]
      |
      v
[ render if needed ] --> repeat

Microtasks are fully drained after every single macrotask.

Playground

Loading editor…
Console

Press Run to execute the code and see output here.

Output Prediction

Predict the output #1

javascript
1console.log('start');
2
3setTimeout(() => console.log('timeout'), 0);
4
5Promise.resolve().then(() => {
6 console.log('promise 1');
7 Promise.resolve().then(() => console.log('promise 2'));
8});
9
10console.log('end');

Predict the output #2

javascript
1async function f() {
2 console.log('1');
3 await Promise.resolve();
4 console.log('3');
5}
6f();
7console.log('2');

Coding Exercises

Order the logs without running the code

Medium

Given the snippet below, write down the exact console output order. Then implement a function scheduleOrdered() that reproduces the order A, D, B, C using one setTimeout and one Promise.

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

(The snippet prints A, D, C, B. Your task: make it print A, D, B, C instead.)

Interview Questions

1Explain the difference between a macrotask and a microtask.

Macrotasks (tasks) include setTimeout, setInterval, I/O, and UI events; the event loop runs one per iteration. Microtasks include promise callbacks (.then/.catch/.finally), queueMicrotask, and await continuations; the loop runs all of them after each task and before the next task. Therefore microtasks have higher priority: a resolved promise callback runs before a setTimeout(fn, 0) scheduled at the same time.

Asked at:GoogleMetaAmazonMicrosoftNetflix

Follow-ups

  • What is microtask starvation?
  • Where does await fit in?
  • Does the browser render between microtasks?
2Why does setTimeout(fn, 0) not run immediately?

setTimeout schedules a macrotask; it doesn't pause the current code. The callback can only run after (a) the current synchronous code finishes and (b) all pending microtasks are drained. The 0 is a minimum delay, not a guarantee — nested timers are also clamped to ~4 ms in browsers.

Quiz

1. Which runs first when scheduled together: a resolved Promise.then or setTimeout(fn, 0)?

2. The code after an `await` runs as…

Summary

  • The event loop runs queued callbacks when the call stack is empty.
  • Order: all synchronous code → drain ALL microtasks → one macrotask → repeat.
  • Microtasks (promises, await, queueMicrotask) outrank macrotasks (timers, I/O, events).
  • setTimeout(fn, 0) queues a macrotask; it never runs before pending microtasks.

Cheat Sheet

Loop: sync (run-to-completion) → drain ALL microtasks → 1 macrotask → repeat.

Microtasks: .then/.catch/.finally, queueMicrotask, await continuation. Run all each turn.

Macrotasks: setTimeout, setInterval, I/O, UI events. Run one per turn.

Rules: sync first · microtasks before next macrotask · setTimeout(fn,0) = macrotask · beware microtask starvation.