JavaScript runs one piece of code at a time, on a single call stack. The event loop decides what runs next: first your whole script, then every waiting microtask (such as promise callbacks), then one task (such as a timer callback), and then it repeats. That is why a setTimeout(…, 0) callback runs after code that comes later in the file.
Try it
This example mixes a timer and a promise. Press Next step to move one step at a time, or Play to run it. Watch the call stack, the two queues and the console.
Code (current line highlighted)
Call stack (top first)The function running right now is on top. Calling a function adds a frame; returning removes it. Only one frame runs at a time.
Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.
Microtask queue (next first)Promise callbacks (.then, code after await, queueMicrotask) wait here. Once the call stack is empty, all of them run before the next task.
Task queue (next first)Timer callbacks wait here. The event loop takes one task, runs it, then empties the microtask queue before taking the next.
Console output
Scope and variables (yellow = changed this step)
Timeline (click a block or use the arrow keys)Script, function calls and returnsawait and microtasksTimers and tasksConsole outputErrors
Step by step: what the example does
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");console.log("start")runs right away and prints start.setTimeouthands its callback to a Web API: the browser starts a timer outside JavaScript. The script does not wait; it moves on.Promise.resolve()is already resolved, so its.thencallback goes straight into the microtask queue. It can't run yet, because the script is still on the call stack.console.log("end")prints end. The script is finished and the call stack is empty.- The event loop now empties the microtask queue: the
.thencallback runs and prints promise. - With no microtasks left, the event loop takes one task from the task queue: the timer callback, which prints timeout.
The output is start, end, promise, timeout, even though the timer was created first and asked for a delay of 0 ms.
The parts of the event loop
- Call stack
- Where JavaScript code actually runs, one function frame at a time. Nothing else can run until the stack is empty.
- Web APIs
- Features the browser (or Node.js) provides outside the JavaScript engine, such as timers, network requests and DOM events. They wait in the background and hand a callback back when they are ready.
- Task queue (also called the macrotask queue)
- Callbacks from timers, user events and I/O wait here for their turn.
- Microtask queue
- Promise callbacks (
.then,.catch,.finally), the code after anawait, andqueueMicrotaskcallbacks wait here. This queue has priority over the task queue. The microtasks and macrotasks guide covers which API uses which queue. - Event loop
- The rule that moves the next piece of work onto the call stack whenever it becomes empty.
One turn of the loop
┌─► 1. Run one task (the very first task is your whole script)
│ 2. Run ALL microtasks, including any added while they run
│ 3. Browser only: update the screen if a new frame is due
└── 4. Take the next task and repeatStep 2 is the important difference between the two queues. The event loop takes tasks one at a time, but it keeps running microtasks until that queue is empty. If a microtask keeps scheduling new microtasks, tasks and screen updates never get a turn, and the page freezes.
Why setTimeout(fn, 0) is not instant
A delay of 0 means "as soon as possible", not "now". The callback has to wait for three things:
- the code that is running now must finish and leave the call stack empty;
- every waiting microtask runs first;
- the timer itself has a minimum delay. Browsers raise very short delays, for example to at least 4 ms once timers are nested about five levels deep, and they slow timers down further in background tabs.
The number you pass to setTimeout is a minimum waiting time, never a promise of exact timing.
Long-running code blocks everything
While the call stack is busy, the event loop cannot hand it anything else, not even a timer that is already due:
setTimeout(() => console.log("timer"), 0);
let total = 0;
for (let i = 0; i < 4; i++) {
total = total + i;
}
console.log("loop done", total);The timer is ready almost immediately, but its callback only runs after the loop has finished and loop done 6 has been printed. A loop that takes seconds would delay the timer by seconds, and in a browser it would also freeze clicks, typing and painting. Long work is usually split into smaller pieces, or moved to a Web Worker, which runs on its own thread. To step through it, choose A loop blocks timers in the example menu above.
Common mistakes
- "setTimeout with 0 runs next." Every microtask that is already waiting runs before it.
- "Promises run in the background." Waiting can happen in the background (a timer, a network request), but promise callbacks themselves run on the same single call stack, one at a time.
- "try/catch around setTimeout catches its errors." The callback runs later, in a different task, after the
tryblock has already finished. The try/catch and timers example shows this.
What LoopLens simulates
LoopLens runs your code in its own interpreter so it can record every step. The order of console output matches real JavaScript; its examples are checked against Node.js. The call stack, the Web APIs, both queues and the clock are simulated: time is virtual and jumps straight to the next timer, so a 100 ms timer doesn't really take 100 ms. Screen updates, user events, network requests, minimum timer delays, and Node.js-only features such as process.nextTick and setImmediate are not modeled.