LoopLens

How the JavaScript event loop works

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.

Test yourself. When this is on, LoopLens pauses before every step that prints to the console and asks what it will print. Pick an answer to check it, or choose Just show me.Step 0 of 15 · virtual clock 0 ms
Event loop:Script (now)MicrotasksTask, then it repeats.The event loop runs the whole script first. Then it runs every microtask, then one task, and repeats.

Code (current line highlighted)

1current line: console.log("start");
2setTimeout(() => console.log("timeout"), 0);
3Promise.resolve().then(() => console.log("promise"));
4console.log("end");

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.

main script

Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.

empty
The whole script runs as ONE task on the call stack. Lines run top to bottom.

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.

empty

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.

empty

Console output

nothing yet

Scope and variables (yellow = changed this step)

no variables yet

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");
  1. console.log("start") runs right away and prints start.
  2. setTimeout hands its callback to a Web API: the browser starts a timer outside JavaScript. The script does not wait; it moves on.
  3. Promise.resolve() is already resolved, so its .then callback goes straight into the microtask queue. It can't run yet, because the script is still on the call stack.
  4. console.log("end") prints end. The script is finished and the call stack is empty.
  5. The event loop now empties the microtask queue: the .then callback runs and prints promise.
  6. 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 an await, and queueMicrotask callbacks 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 repeat

Step 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 try block 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.

Next: Microtasks vs macrotasks in JavaScript →

Open the visualizer with your own code