The event loop is how single-threaded JavaScript handles asynchrony. Synchronous code runs on the call stack; async callbacks are handed to Web APIs and later queued. After each macrotask the engine drains the entire microtask queue before taking the next macrotask — which is exactly why Promise callbacks run before setTimeout(fn, 0).
Last updated: 2026-09-17 · Based on The HTML specification's event loop as implemented in browsers; Node.js divides its loop into phases and behaves slightly differently
The event loop shows up in almost every front-end interview, and most explanations jump straight to “macrotasks and microtasks” without saying what those have to do with the loop itself. This is an attempt to build it up in order.
This is the English companion to the Chinese original. Same content, written for English readers.
Start with a question
What does this print?
1 | console.log('1') |
The answer is 1 → 4 → 3 → 2. If you can explain why without hesitating, you can skip the rest.
JavaScript is single-threaded
One thing at a time. Yet we write timers, network requests and event listeners that clearly do not block the page. The event loop is what reconciles those two facts.
The four pieces
The call stack. Where synchronous code runs. Frames push on, execute, pop off — last in, first out.
Web APIs. Capabilities the browser provides, not the JS engine: setTimeout, fetch, DOM events. This is the key point behind the setTimeout(fn, 0) confusion — the timer is the browser’s job, not the engine’s.
The macrotask queue. Where completed Web API callbacks line up waiting for the stack to clear.
The microtask queue. A separate, higher-priority queue. Promise callbacks land here.
The loop itself
Two rules cover it:
- Run synchronous code until the call stack is empty.
- Drain the entire microtask queue, then take one macrotask. Repeat.
That asymmetry — all microtasks, one macrotask — is the whole answer to the ordering question.
Walking through the example
| Step | What happens | Output |
|---|---|---|
| 1 | console.log('1') runs synchronously |
1 |
| 2 | setTimeout hands its callback to the browser |
— |
| 3 | Promise.resolve().then queues a microtask |
— |
| 4 | console.log('4') runs synchronously |
4 |
| 5 | Stack empties; engine drains microtasks | 3 |
| 6 | Engine takes one macrotask | 2 |
Why this matters beyond interviews
Starving the renderer. Rendering is itself scheduled by the loop. A microtask that keeps queueing more microtasks never lets the loop reach the render step, and the page freezes — no error, just an unresponsive tab.
await is a microtask boundary. Everything after an await is a microtask continuation, so it runs before any pending timer, even one that was scheduled earlier.
Timer delays are a floor, not a promise. setTimeout(fn, 100) means “not before 100ms”. If the stack is busy or microtasks are piling up, it will be later — sometimes much later.
Further reading
The Chinese original covers the same material with more worked examples: Event Loop 到底是什么.
Frequently asked questions
Why doesn't setTimeout with 0 delay run immediately?
setTimeout does not execute in the JS engine at all. It hands the callback to the browser, which starts a timer. Even at 0ms the callback only joins the macrotask queue afterwards, and it still has to wait for all remaining synchronous code and the entire microtask queue to finish.
Which APIs produce macrotasks and which produce microtasks?
Macrotasks: setTimeout, setInterval, I/O, UI rendering, event callbacks. Microtasks: Promise then/catch/finally callbacks, queueMicrotask, MutationObserver. Code after an await inside an async function is also a microtask.
Does the engine drain the whole microtask queue or take just one?
The whole queue. After each macrotask the engine keeps pulling microtasks until the queue is empty, including microtasks created during that draining. This is why an endlessly self-scheduling microtask freezes the page — the loop never gets back to rendering.
What is the output order of console.log, setTimeout, Promise.then, console.log?
1, 4, 3, 2. Both console.log calls are synchronous so they print first in source order. Then the microtask queue drains and the Promise callback prints 3. Only then does the loop take the setTimeout callback from the macrotask queue and print 2.





