What is the JavaScript event loop? How the call stack, task queue, and microtask queue work
The JavaScript event loop is the runtime model that lets single-threaded JavaScript run asynchronous code. Learn how the call stack, the task queue, and the microtask queue cooperate, and why Promise callbacks fire before setTimeout.
On this page
JavaScript is a single-threaded language: one call stack, one thing running at a time. The event loop is the runtime mechanism that lets that single thread handle asynchronous work — timers, network requests, user input, promise callbacks — without ever blocking. Every JavaScript host (browsers, Node.js, Deno, workers) implements some version of it, and once you understand the loop, behaviour that looks "weird" — like setTimeout(fn, 0) running after a Promise.then(fn) — becomes predictable.
The short version: JavaScript runs synchronous code on the call stack; when an async operation finishes, its callback is queued; the event loop's only job is to keep pulling work from the queues whenever the stack is empty. Modern hosts split queued work into two queues — a task queue (macrotasks) and a microtask queue — and microtasks always drain before the next task.
The pieces
The runtime that runs your JavaScript is built from a few small, cooperating parts:
- Call stack — a LIFO (last-in, first-out) stack of execution contexts. The function you are inside is on top; the function that called it is below. When a function returns, its frame pops.
- Heap — where objects and closures live. Mostly out of the picture for the loop itself.
- Web APIs / Node APIs — the host's async primitives:
setTimeout,fetch,addEventListener, file I/O, network sockets. These run outside the JS engine; when they finish, they place a callback into a queue. - Task queue (a.k.a. macrotask queue) — a FIFO queue of tasks. Tasks include the initial script, event callbacks,
setTimeout/setIntervalcallbacks, I/O completion callbacks. - Microtask queue — a FIFO queue of microtasks. Microtasks include
Promise.then/catch/finallycallbacks,queueMicrotask()callbacks, andMutationObservercallbacks. - Event loop — a simple loop that, on each iteration, runs one task, then drains the microtask queue, then optionally renders, then repeats.
The "host" here matters: the exact list of what counts as a task vs a microtask is defined by HTML in browsers and by the libuv model in Node.js. The two are not identical, but the shape is.
How one iteration works
In HTML terms, each iteration of the event loop does roughly this:
- Pick the oldest task from the task queue. Run it to completion — that is, the call stack is never re-entered until the current task's top-level synchronous code finishes.
- After the task ends, drain the microtask queue: keep popping microtasks and running them until the queue is empty. New microtasks scheduled while draining also run in the same pass.
- If the document is visible, perform rendering and paint.
- Go back to step 1 and wait for the next task.
Two guarantees follow from that shape:
- Run-to-completion — a task cannot be interrupted by another task. Once it starts, it finishes synchronously.
- Microtasks drain after every task — a microtask queued at any point will run before the next task starts, even if it was scheduled during rendering work or during the task itself.
That second guarantee is the one that explains most "why did the promise run first?" questions.
A worked example
Consider this snippet, which is the canonical event-loop interview question:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Walk through it line by line:
console.log("1")runs synchronously on the call stack. Output:1.setTimeout(..., 0)schedules a task. The host's timer service remembers the callback and the (zero) delay; when the timer expires, the host places the callback into the task queue. Nothing runs yet.Promise.resolve().then(...)schedules a microtask via the promise machinery. The callback is placed into the microtask queue.console.log("4")runs synchronously. Output:4.- The current task (the initial script) is now complete. The event loop drains the microtask queue: the promise callback runs. Output:
3. - The event loop picks the next task — the
setTimeoutcallback — and runs it. Output:2.
Final output: 1 4 3 2. The key idea: microtasks (promises) beat tasks (timers) by design.
Why microtasks first?
The HTML spec made microtasks higher priority than tasks for a practical reason: a promise resolution is supposed to feel synchronous to user code. If you write:
const result = await fetch(url);
use(result);
you want the use(result) callback to run as soon as fetch finishes — not to wait behind an unrelated setTimeout from another piece of code. Draining the microtask queue after every task makes that promise nearly always true, while still letting tasks carry the bulkier work (I/O, rendering, events).
The same rule is what makes queueMicrotask(fn) a useful primitive: it lets you schedule a callback to run after the current synchronous code, but before the next task — a guaranteed "very soon" without blocking.
How this looks in Node.js
Node.js implements the same model on top of libuv, with a few extra phases to handle I/O more fairly. The user-facing story is the same: microtasks (promise callbacks) run after the current task and before the next I/O or timer phase. Node also exposes process.nextTick(fn), which is a separate, even more eager queue that runs after the current operation and before any microtasks — a detail worth knowing because mixing it with promise code can produce surprising ordering.
If you debug "the server responded first but the promise logged after the timer", the loop is usually where the answer lives: the timer had a 0 ms minimum, the I/O callback was scheduled into a later phase, and the microtask drained in between.
Common pitfalls
- Long synchronous work blocks the loop. A
whileloop that takes 500 ms freezes input, timers, and rendering. The fix is the usual: chunk the work, or move it off the main thread with a Web Worker. - Stack overflow on deeply nested calls. The call stack has a finite size; recursive code without a base case can throw
RangeError: Maximum call stack size exceeded. Microtasks scheduled recursively from inside their own drain pass can also starve the loop — each new microtask runs before the queue is "empty", so rendering and tasks never get a turn. setTimeout(fn, 0)is "after tasks and microtasks", not "immediately". A 0 ms delay still yields to the loop. UsequeueMicrotask(fn)if you really want the soonest possible callback.- Order across I/O is not guaranteed by delay alone. Two
fetchcalls that complete at the same time will resolve in the order their responses arrived at the network, not the order you called them. Don't rely on the literal source order.
FAQ
What is the difference between a task and a microtask?
A task is the unit of work the event loop pulls on each iteration: the initial script, an event callback, a setTimeout callback, or an I/O completion. A microtask is a smaller, higher-priority unit that always runs after the current task and before the next task. Microtasks include promise callbacks (then/catch/finally), queueMicrotask() callbacks, and MutationObserver callbacks. The event loop drains the entire microtask queue after every task, so a queued microtask is guaranteed to run before the next task starts.
Why does Promise.then run before setTimeout(fn, 0)?
Promise callbacks are microtasks; setTimeout callbacks are tasks. The event loop drains the microtask queue after every task and before pulling the next task from the task queue. So in a single iteration, a microtask scheduled during the current task — like the one a resolved promise places — always runs before any newly-pending task, including one whose timer has already expired. This is a deliberate HTML spec choice: it makes promise resolution feel synchronous to user code.
Is the event loop part of JavaScript?
No. The event loop is provided by the host environment — the browser (per the HTML specification) or Node.js (per its libuv-backed model). The JavaScript language itself defines the execution model — a single thread of execution, a stack of execution contexts, a job queue for promise reactions — but the loop that ties it to timers, I/O, and rendering lives in the host. Different hosts can have different scheduler details while keeping the same user-facing shape.
What starves the event loop?
A microtask that schedules another microtask, recursively, will keep the microtask queue non-empty and starve tasks and rendering. Recursive Promise.resolve().then(...) chains are the classic example; a while loop that synchronously schedules microtasks is another. On the task side, a long synchronous computation — parsing a huge JSON, sorting a large array, a tight loop — blocks the call stack, which is the same thing as blocking the loop because nothing else gets a chance to run until the stack unwinds.
Summary
The event loop is the small, repeating rule that turns a single-threaded language into a responsive runtime: run one task, drain all microtasks, render if it's time, repeat. Once you internalize the stack/queue split and the microtask-first rule, the seemingly magical ordering of setTimeout, fetch, and Promise.then becomes obvious — and bugs caused by treating JavaScript as "multi-threaded under the hood" become much easier to spot.
Sources
- MDN — JavaScript execution model (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model
- CC-BY-SA 2.5+)
- MDN — In depth: Microtasks and the JavaScript runtime environment (developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
- CC-BY-SA 2.5+)
- Wikipedia — Event loop (en.wikipedia.org/wiki/Event_loop
- CC BY-SA 4.0)
- Node.js Docs — The Node.js Event Loop (nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
- MIT license)
Share this guide
Related guides
- What is WebAssembly (WASM)? How it works and when to use it
WebAssembly is a binary instruction format for a stack-based virtual machine — a portable compilation target that lets you run C, C++, Rust, and other compiled languages in the browser and beyond. Here's how it works and when it's worth using.
- What is an API? A plain-language guide to how APIs work
What an API is, how REST APIs work over HTTP, the methods you'll actually meet — GET, POST, PUT, PATCH, DELETE — and how to read a response.
- What is a Regular Expression (regex)? A practical guide
A regular expression (regex or regexp) is a sequence of characters that forms a search pattern for matching and manipulating text. Built on a formal mathematical foundation and common to almost every programming language and tool, regex lets you validate input, find and replace text, extract data, and transform strings with a compact, declarative syntax. This guide explains what regex is, its core syntax, how it is implemented, and how to use it safely.