The Event Loop
One thread, a queue, and a rule about when it drains — the model that explains every 'why did this run last?'.
45 minDifficulty 3/5runtime · asyncReviewedRead and accepted by a person.
Before this
Why this exists
You add a `console.log` inside a `setTimeout(..., 0)` and it prints *after* code written below it. You wrap something in `Promise.resolve().then()` and it runs before the timeout, despite being written later. None of this is random — there's a precise, learnable schedule, and knowing it is the difference between debugging async code and guessing at it.
The mental model
One chef in a kitchen. He never waits at the oven — he puts something in, sets a timer, and starts the next task. When a timer dings he finishes his current step first, then handles it. One worker, no idle waiting, and nothing is ever interrupted mid-step.
How it works
One thread, and functions run to completion
Your JavaScript runs on a single thread. Once a function starts, it runs to the end — nothing interrupts it partway. That's a real guarantee: no other code can observe your object in a half-updated state, so you never need locks around ordinary code. The price is that a slow function blocks *everything*: no clicks, no rendering, no timers, no incoming requests. In a browser the tab freezes; on a server every concurrent user waits.
The waiting doesn't happen in JavaScript
When you call `setTimeout` or start a network request, JavaScript doesn't wait. It hands the job to the runtime — the browser or Node — which has real threads and OS facilities underneath. Your thread immediately continues to the next line. Later, when the work finishes, the runtime puts your callback in a queue. This is why one thread can handle thousands of concurrent connections: it's never the thing doing the waiting.
The loop itself is dumb and simple
The event loop does one thing forever: if the call stack is empty, take the next callback from a queue and run it. That's the entire algorithm. The subtlety isn't the loop — it's that there is more than one queue, and they don't have equal priority.
Microtasks jump the queue — all of them
Promise callbacks go on the **microtask** queue. Timers, I/O and events go on the **macrotask** queue. After each macrotask, the loop drains the *entire* microtask queue before taking another macrotask — and microtasks added during that drain get processed too, in the same pass. This is why `Promise.resolve().then()` beats `setTimeout(..., 0)`. It's also a trap: a promise chain that keeps scheduling more promises can starve the macrotask queue completely and freeze the page, without ever running an infinite `while` loop.
The mechanism
Note the asymmetry: **all** microtasks, but only **one** macrotask per turn. That's the whole priority story. ```js console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // 1, 4, 3, 2 ``` `1` and `4` are synchronous — they run first, in order. Then the stack empties, microtasks drain, so `3`. Only then does a macrotask run: `2`.
flowchart TD
A[Call stack empty?] -->|no| A
A -->|yes| B[Drain ALL microtasks]
B --> C{More microtasks?}
C -->|yes| B
C -->|no| D[Render if browser]
D --> E[Take ONE macrotask]
E --> F[Run it to completion]
F --> AWhat people get wrong
- setTimeout with 0 runs the callback immediately.
- It runs after the current synchronous code and after every pending microtask. The delay is a minimum wait before becoming eligible, not a promise about when it runs.
- async/await makes code run on another thread.
- It's the same single thread. await pauses the function and returns control to the loop. Concurrency here means interleaving, not parallelism. CPU-heavy work in an async function still blocks everything.
- Promises are faster than callbacks.
- They're scheduled at a higher priority, which is different from being faster. Microtasks are processed before the next macrotask. That's an ordering guarantee, not a speed improvement.
- Node is single-threaded, so it can't use multiple cores.
- Your JavaScript runs on one thread, but Node has a thread pool for file I/O and crypto, and you can add worker threads or cluster processes. The single thread is about your code's execution model, not about the whole process's use of the machine.
When not to use it
- You have genuinely CPU-bound work — image processing, big sorts, crypto.
- A Web Worker in the browser, or worker_threads in Node. The event loop helps with waiting, not with computing.
- You need hard real-time timing guarantees.
- Not JavaScript. Timer callbacks fire when the loop gets to them, which is after whatever is currently running finishes.
Terms
- Call stack
- — The stack of function calls currently executing. The loop only acts when it's empty.
- Macrotask
- — A task from the main queue — timers, I/O, UI events. One runs per loop turn.
- Microtask
- — A promise callback or queueMicrotask job. The whole queue drains between macrotasks.
- Starvation
- — When continuously-scheduled microtasks prevent macrotasks from ever running.
- Run-to-completion
- — The guarantee that a function finishes before any other JavaScript runs.
- Blocking
- — Occupying the thread so the loop can't process anything else.
In an interview
What's the output order of a mix of sync code, setTimeout and promises?
- all synchronous code first, in written order
- then the entire microtask queue drains
- then one macrotask, then microtasks drain again
- the number in setTimeout is a minimum delay, not a scheduled time
Node is single-threaded — how does it handle thousands of concurrent requests?
- the thread never waits; I/O is delegated to the runtime and the OS
- callbacks are queued and run when the stack is empty
- concurrency comes from interleaving, not parallelism
- CPU-bound work breaks the model and needs worker threads
Can you recall it?
Why does a promise callback run before a setTimeout of 0, even when the timeout was scheduled first?
Connected ideas
- The Node.js Runtime — JavaScript outside the browser: modules, streams, and a single thread doing a lot of I/O.
- Concurrency vs Parallelism — Dealing with many things at once vs doing many things at once — not the same, and the distinction matters.
- Effects & Lifecycles — Synchronising with the outside world — and why you probably don't need an effect.
Also part of
This idea matters in more than one area — which is usually why it matters.