Every piece of the event loop, explained in the voice you picked up top — switch anytime with the toggle or keys 123. The pictures these words describe are one click away in every entry.
The chef. There is exactly one, and they cook one dish at a time, always finishing the dish on top of the pile before touching the one underneath. When you call a function, it goes on top of the pile; when it finishes, it comes off. If the pile is empty, the chef is free.
Where your code actually runs. Every function call pushes a frame; every return pops one. JavaScript has ONE call stack — one thing executes at a time, and nothing else (no callback, no promise, no timer) can run until the stack is empty.
The execution context stack. Each invocation pushes an execution context holding its environment records and code evaluation state. The whole model is single-threaded per agent: an event loop processes one task at a time, and a task runs its contexts to completion.
The waiter. Some jobs — timers, fetching things from far away — aren't cooking at all, so the chef hands them to the waiter and keeps cooking. The waiter works on the side and, when a job is done, drops it into an order line for the chef to pick up later.
Browser-provided machinery that lives OUTSIDE JavaScript: timers, fetch, DOM events. setTimeout does not run your callback — it asks the browser to wait, and the browser later drops the callback into the task queue. This is how a single-threaded language appears to do two things at once.
Host capabilities defined by HTML and companion specs, running off the JS thread. When an operation completes (a timer fires, a network response arrives), the user agent queues a task from the appropriate task source; the callback itself still executes on the main thread, one task at a time.
The regular order line. Finished waiter-jobs stand here in order and wait. The chef only takes the first one in line — and only when the current dish is completely finished and the VIP line is empty.
Where setTimeout callbacks, DOM events, and other "macrotasks" wait. The loop takes ONE task per go-around, and only when the stack is empty. A callback in this queue can wait a long time behind a slow script — its timer expiring only means "ready", never "running".
Task queues, per the HTML event loop — actually sets, keyed by task source (timers, user interaction, networking…). Each loop iteration, the user agent picks one runnable task from one queue. Ordering is guaranteed within a source, not across sources.
The VIP line. Promise follow-ups stand here. Whenever the chef finishes a dish, they serve EVERYONE in the VIP line — even VIPs who joined while serving other VIPs — before taking a single order from the regular line.
The priority lane for promise callbacks and queueMicrotask. It drains COMPLETELY after the current script (and after every task), including microtasks queued by other microtasks — before any setTimeout callback gets a chance. That is the whole reason promise callbacks beat timers.
A single FIFO queue per event loop, drained at the microtask checkpoint: after a task's script runs to completion, "perform a microtask checkpoint" loops until the queue is empty — newly enqueued microtasks included. Promise reaction jobs and queueMicrotask both land here.
The rule the chef lives by, repeated forever: finish the current dish → serve the whole VIP line → take ONE order from the regular line → repeat. That's it. Everything on this site is just this rule, drawn.
Not a thing in your code — a loop the browser runs around it: run a task to completion, drain all microtasks, maybe render, take the next task. It only gets to act when the call stack is empty, which is why blocking the stack freezes everything.
The processing model in HTML §8.1.7: each iteration selects one runnable task, runs it, performs a microtask checkpoint, then (in a window event loop) may update the rendering. Every window and worker gets one; same-origin windows can share one.
Regular line vs VIP line. A VIP who arrives LATER still gets served before the whole regular line. Fair? No. The rule? Yes.
The single most-asked interview question on this topic. A microtask queued after a task still runs first, because the loop drains all microtasks between tasks. setTimeout(fn, 0) therefore means "after the current script AND all pending microtasks" — never "now".
One task per iteration; a full microtask checkpoint between them. The checkpoint drains recursively, so reaction jobs enqueued during the checkpoint also run before the next task. Tasks from the timer task source cannot preempt or interleave with this.
Telling the waiter "in zero minutes" still means the waiter takes it, walks away, and brings it back through the regular line. The chef finishes everything on the pile — and the whole VIP line — first.
The delay is a MINIMUM, not a schedule. 0ms means "queue the callback as soon as possible" — which is after the current script finishes and after all microtasks drain. Under load, a 0ms callback can be seconds late; a timer never interrupts running code.
Timer initialisation steps: after the timeout, queue a task from the timer task source. Nested timers beyond depth 5 are clamped to a ≥4ms delay; inactive documents throttle further. The expiry only makes the task runnable — execution waits for its loop iteration.
A promise is an IOU for a value. Its .then follow-up is a VIP order: it never runs immediately — even if the IOU is already paid — but it beats everything in the regular line.
A .then callback NEVER runs synchronously — even on an already-resolved promise it is queued as a microtask. Each .then returns a new promise, so chains run one microtask after another, all before the next task. Promises never touch Web APIs; they live entirely in JS.
PerformPromiseThen creates reaction records; settling a promise enqueues reaction jobs onto the microtask queue via HostEnqueuePromiseJob. Resolution is always asynchronous by construction — ECMAScript guarantees the "no synchronous .then" invariant.
async function main() {
await Promise.resolve()
console.log('after') // runs as a microtask
}
A recipe with a "wait here" line in it. At await, the chef sets the dish aside mid-recipe and cooks other things; when the waited-for part is ready, the REST of the recipe joins the VIP line.
Promises in a costume. Everything before the first await runs synchronously. At await, the function SUSPENDS — it leaves the call stack, and the rest of its body is queued as a microtask when the awaited promise settles. await never blocks the thread; it yields it.
Await performs roughly PromiseResolve(value).then(continuation): the async function's context is popped and its resumption is enqueued as a reaction job. Even awaiting an already-settled value costs (at least) one microtask tick — control always returns to the caller first.
A direct pass into the VIP line — no IOU needed. Same line the promise follow-ups use.
The explicit way to queue a microtask without creating a promise. Same queue, same drain-completely rule as .then callbacks. Use it when you need "after this script, before any task" — and beware: it can starve tasks just like promises can.
Defined in HTML: queues a microtask directly on the event loop's microtask queue — the same queue promise reaction jobs use via HostEnqueuePromiseJob. Runs during the next microtask checkpoint.
One dish that never finishes. The chef can't serve either line, the waiter's finished jobs pile up, and the whole restaurant stands still — buttons, scrolling, everything.
A long synchronous job (a heavy loop, a giant JSON.parse) keeps the stack busy, so NOTHING else runs: no timers, no promise callbacks, no clicks, no repaints. This is why "the page froze" and "my timer was late" are the same bug. Break work up, or move it to a Worker.
Tasks run to completion; the rendering update is a step of the same loop iteration. While a task monopolises the thread, no microtask checkpoint occurs, no task is selected, and no render opportunity arises — the document becomes non-responsive by construction.
function spin() { queueMicrotask(spin) } // tasks never run again
VIPs who keep inviting more VIPs. Because the chef must empty the VIP line before taking a regular order, the regular line never moves. Priority has a dark side.
Because the microtask queue drains COMPLETELY — including microtasks queued during the drain — a microtask that always queues another one blocks every task forever: timers, clicks, rendering. Promise loops can do this by accident; setTimeout chains cannot (each round is a new task).
The microtask checkpoint loops "while the queue is not empty", with no budget or fairness bound. An unbounded reaction-job chain therefore prevents the loop from ever selecting the next task or reaching update-the-rendering. Tasks re-queued via timers do not share this hazard.
Between orders, the chef sometimes tidies the counter so customers can see what's ready. If the chef never gets a break between dishes, the counter never updates — that's a frozen screen.
Rendering (style → layout → paint) happens BETWEEN tasks, not during them — roughly 60 times a second, after microtasks drain. requestAnimationFrame callbacks run just before that. Change the DOM 1000 times in one task and the user sees only the final state, once.
Update-the-rendering is a step in the loop iteration after the microtask checkpoint, gated on a rendering opportunity (display refresh, throttling). It runs rAF callbacks, then style/layout/paint. Multiple tasks may run between opportunities; a long task skips them entirely.