The Event Loop (Introduction)
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:
- If the call stack is not empty, keep running (synchronous code has priority).
- When the stack is empty, run every queued microtask (draining the queue, including microtasks scheduled by microtasks).
- Run one macrotask (a.k.a. task) from the task queue.
- Go back to step 2 (drain microtasks again).
- In the browser, render between iterations if needed.
Macrotasks vs microtasks
| Macrotasks (tasks) | Microtasks | |
|---|---|---|
| Examples | setTimeout, setInterval, I/O, UI events, setImmediate (Node) | Promise.then/catch/finally, queueMicrotask, await continuation, MutationObserver |
| When they run | one per loop iteration | all of them, after each task |
| Priority | lower | higher |
The golden rules
- Synchronous code always runs first, to completion.
- All microtasks run before the next macrotask.
setTimeout(fn, 0)does not run immediately — it queues a macrotask.await xpauses 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
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 ] --> repeatMicrotasks are fully drained after every single macrotask.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
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
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
MediumGiven 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.
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.