JavaScript event loop simulator

JavaScript runs one thing at a time, and the event loop decides what runs next: the rest of the script, a promise callback, a timer or an animation frame. Pick a program below and step through it to see why the console prints what it does. Further down: tasks and microtasks, async/await, rendering, and the Node.js event loop.

Steps
0
Tasks run
0
Microtasks run
0
Clock
0 ms
Program
Run
The classic question: which callback runs first?
  1. 1console.log('script start');
  2. 2
  3. 3setTimeout(() => {
  4. 4 console.log('setTimeout');
  5. 5}, 0);
  6. 6
  7. 7Promise.resolve()
  8. 8 .then(() => console.log('promise 1'))
  9. 9 .then(() => console.log('promise 2'));
  10. 10
  11. 11console.log('script end');
Call stack
  • main script
Microtask queue
  • empty
Timers and task queue
  • empty
Animation frame callbacks
  • empty
Console
  1. nothing printed yet

Running the script

What happened
  1. 0 msThe script starts running as the first task. Everything else waits until the call stack is empty.

How to use the simulator

Choose a program. Each one is a few lines of real JavaScript. Press Step to run one statement or one move of the event loop at a time: the highlighted line is the code running now, the four lanes show what is waiting, and the console shows what has been printed. Play steps every 0.7 seconds; Run to the end finishes at once.

Turn on Hide output until the end to test yourself: work out the order of the console lines before you run the program. The answers are at the end of the article.

The model follows the event loop in the HTML standard: the script runs as the first task; after it, and after every later task, the whole microtask queue is emptied; timers become tasks when they are due; and a frame is rendered, running requestAnimationFrame callbacks, every 1/60 of a second when there is something to draw. Each program was also run in Chromium and Node.js, and the console output matched the model's in every case (Node.js has no requestAnimationFrame, so that program ran in Chromium only).

What is the JavaScript event loop?

The event loop is how a JavaScript environment decides which code to run next. JavaScript code runs on one thread with one call stack, and a piece of code always runs to completion before anything else starts. Everything that should happen later, such as a timer firing, a button being clicked or a network response arriving, waits in a queue. Each time the call stack becomes empty, the event loop picks the next thing to run.

The JavaScript language itself defines promises and jobs, but not timers, clicks or rendering. Those belong to the host: in browsers, the event loop is specified by the HTML standard; in Node.js, it is provided by the libuv library. Both follow the same basic cycle:

  1. Take one task from a task queue and run it until the call stack is empty.
  2. Run every microtask in the microtask queue, including ones added along the way.
  3. In a browser, if it is time for a new frame, run requestAnimationFrame callbacks, update the layout and paint.
  4. Repeat.

The call stack and run-to-completion

The call stack records which functions are running: calling a function pushes a frame, returning pops it. JavaScript never interrupts a running function to run another callback. A click handler, a timer callback or a promise callback can only start when the stack is empty. This is called run-to-completion, and it is why JavaScript code does not need locks around ordinary variables: no other callback can change them halfway through a function.

The other side of run-to-completion is that a long-running function holds up everything else. While it runs, timers are late, clicks wait, and the page is not repainted.

Tasks (macrotasks)

A task is a unit of work the event loop starts from an empty stack. The HTML standard calls them tasks; many articles call them macrotasks to contrast them with microtasks. Running the main script is a task. So are:

  • a setTimeout or setInterval callback, once its delay has passed;
  • an event handler for a click, a key press or a network response;
  • a message from postMessage, a MessageChannel or a Web Worker.

The event loop runs one task per turn, then empties the microtask queue, then may render. A browser can keep several task queues, for example one for user input and one for timers, and choose between them to keep the page responsive, but tasks from the same source always run in the order they were queued. Timers are queued when they are due, so a 0 ms timer set after a 100 ms timer still runs first.

Microtasks

A microtask is a callback that must run as soon as the current code finishes, before the event loop does anything else. They come from:

  • promise reactions: the callbacks passed to .then, .catch and .finally, queued when the promise settles (or straight away if it already has);
  • await: the rest of an async function after an await;
  • queueMicrotask(fn);
  • MutationObserver callbacks.

The event loop runs a microtask checkpoint whenever the call stack becomes empty: after each task, and also after each event listener or callback that the browser calls. At a checkpoint it runs microtasks until the queue is empty, and microtasks queued during the checkpoint run in the same checkpoint. That is why all promise callbacks run before the next setTimeout callback, and why a chain of .then calls completes without anything else interleaving, except other microtasks: the "Two promise chains" program shows two chains taking turns.

Event loop example, step by step

This is the simulator's own trace of the first program, the question asked in many JavaScript interviews:

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(() => console.log('promise 1'))
  .then(() => console.log('promise 2'));

console.log('script end');
#What happensCall stackMicrotasksTasks and timersPrinted so far
1console.log prints "script start".main script––script start
2setTimeout registers a callback with the browser's timer. In 0 ms it becomes a task in the task queue.main script–setTimeout callback (0 ms)script start
3The promise is already resolved, so .then puts its callback straight into the microtask queue. The next .then waits until this callback has returned.main scriptthen callback 1setTimeout callback (0 ms)script start
4console.log prints "script end".main scriptthen callback 1setTimeout callback (0 ms)script start, script end
5main script returns and is popped off the call stack. The call stack is empty, so the microtask queue is emptied before anything else.(empty)then callback 1setTimeout callback (0 ms)script start, script end
6Microtask checkpoint: then callback 1 is taken from the microtask queue and run.then callback 1–setTimeout callback (0 ms)script start, script end
7console.log prints "promise 1".then callback 1–setTimeout callback (0 ms)script start, script end, promise 1
8then callback 1 returns and is popped off the call stack. Its promise is now resolved, so the next .then callback joins the microtask queue. The call stack is empty, so the microtask queue is emptied before anything else.(empty)then callback 2setTimeout callback (0 ms)script start, script end, promise 1
9Microtask checkpoint: then callback 2 is taken from the microtask queue and run.then callback 2–setTimeout callback (0 ms)script start, script end, promise 1
10console.log prints "promise 2".then callback 2–setTimeout callback (0 ms)script start, script end, promise 1, promise 2
11then callback 2 returns and is popped off the call stack. The call stack is empty.(empty)–setTimeout callback (0 ms)script start, script end, promise 1, promise 2
12At 0 ms the event loop takes the next task: the setTimeout callback (0 ms). Only one task runs per turn of the loop.setTimeout callback (0 ms)––script start, script end, promise 1, promise 2
13console.log prints "setTimeout".setTimeout callback (0 ms)––script start, script end, promise 1, promise 2, setTimeout
14setTimeout callback (0 ms) returns and is popped off the call stack. The call stack is empty.(empty)––script start, script end, promise 1, promise 2, setTimeout

The 0 ms timer is due almost immediately, but it is a task, and the script is still running. When the script finishes, the microtask queue is emptied first, and "promise 2" is queued as soon as the "promise 1" callback returns, so it also runs before the timer. Only then does the event loop take the timer's task.

async and await on the event loop

An async function is a function that returns a promise, and await is a way of writing .then. Calling an async function runs its body synchronously, as part of the caller, up to the first await. At that point the function is suspended: the rest of it is scheduled to run as a microtask once the awaited value is ready, and control goes back to the caller. The "async and await" program shows this: "load: start" prints before "after", but "load: after await" prints after it.

Awaiting a value that is not a promise, or a promise that has already resolved, still suspends the function for one microtask. Awaiting a pending promise suspends it until that promise settles. The exact number of microtask turns an await takes was reduced by a 2019 change to the language specification, which V8 described in its post "Faster async functions and promises"; very old engines can order closely interleaved await and .then code differently.

Because await pauses only its own function, two independent requests written as await a(); await b(); run one after the other. To run them at the same time, start both and then wait: await Promise.all([a(), b()]).

Why setTimeout(fn, 0) is not zero

The delay passed to setTimeout is a minimum, not a promise. The callback becomes a task when the delay has passed and runs when the event loop gets to it: after the current task, after all microtasks, possibly after rendering and other tasks. On top of that:

  • The HTML standard raises the delay to at least 4 ms once timers are nested more than five levels deep, for example a timeout that sets a timeout that sets a timeout.
  • Browsers slow timers in background tabs, usually to at most once a second. Since Chrome 88, chains of timers in a page that has been hidden for five minutes can be limited to once a minute.
  • setInterval does not catch up: if the loop is busy when the interval is due, the callbacks are not stacked up and run in a burst.

For "run this after the current code but as soon as possible", queueMicrotask runs sooner than any timer. To give the browser a chance to render and handle input in between, a task is the right tool, and newer Chromium browsers offer scheduler.postTask and scheduler.yield() for it.

Rendering and requestAnimationFrame

A browser does not repaint after every task. It has a rendering opportunity roughly once per display refresh, every 16.7 ms on a 60 Hz screen, and may skip it if nothing changed or the page is hidden. When it renders, it first runs every requestAnimationFrame callback that was queued before the frame began, then recalculates styles and layout, then paints. Callbacks queued during the frame wait for the next one.

This is why animation code belongs in requestAnimationFrame: it runs once per frame, just before the result is drawn. Changing the page several times within one task shows only the final state, because nothing is painted until the task, its microtasks and the next rendering step have run. The "requestAnimationFrame" program shows the order: synchronous code, then microtasks, then the frame callback and its own microtasks, then a later timer.

Blocking the event loop

Because there is one thread, anything slow on it delays everything else. A task longer than 50 ms counts as a long task in browser performance tools; during it, clicks and key presses cannot be handled and nothing is painted. Since March 2024, Google's Core Web Vitals measure responsiveness with Interaction to Next Paint (INP), which grows when long tasks delay the response to an input.

Microtasks can block too. Since the checkpoint runs until the microtask queue is empty, a promise callback that always queues another one, or a very long chain of them, keeps the loop from rendering or taking the next task, even though no single callback is slow.

The usual fixes are to split long work into chunks with a task between them, to move heavy computation to a Web Worker, or, in Node.js, to a worker thread.

The Node.js event loop

Node.js uses the same microtask rules, but its loop is built on libuv and divided into phases, each with its own queue of callbacks:

  1. timers: due setTimeout and setInterval callbacks;
  2. pending callbacks: some deferred system callbacks, such as certain TCP errors;
  3. poll: wait for and run I/O callbacks, such as file and network reads;
  4. check: setImmediate callbacks;
  5. close callbacks: 'close' events, such as a socket being destroyed.

Between callbacks, Node.js runs the process.nextTick queue first and then the promise microtask queue. So process.nextTick runs before a .then callback queued at the same time, and both run before anything in the next phase. The names are misleading: nextTick runs sooner than setImmediate. In the main module, the order of setTimeout(fn, 0) and setImmediate(fn) depends on timing and can vary between runs; inside an I/O callback, setImmediate always runs first.

Scheduling callBrowserNode.js
process.nextTick(fn)Not availableBefore promise microtasks
Promise.then, await, queueMicrotaskMicrotaskMicrotask
setTimeout(fn, 0)Task when dueTimers phase
setImmediate(fn)Not availableCheck phase
requestAnimationFrame(fn)Rendering stepNot available

Answers to the programs

The console output of each program in the simulator, which is also what Chromium printed:

ProgramOutput, in order
setTimeout vs Promisescript start → script end → promise 1 → promise 2 → setTimeout
async and awaitbefore → load: start → after → load: after await
Two promise chainsA1 → B1 → A2 → B2 → A3
Microtasks between taskstimeout 1 → microtask from timeout 1 → timeout 2
Timer delays0 ms → another 0 ms → 50 ms → 100 ms
Microtasks queued by microtasksmicro 1 → micro 2 → micro 3 → timeout
requestAnimationFramesync → promise → frame → promise in frame → timeout 50 ms
Interview puzzle1 → a1 → 5 → a2 → 3 → 4 → 2

The event loop in interviews

Event loop questions usually show a snippet like the programs above and ask for the output. A reliable method:

  1. Run all synchronous code first, top to bottom, including the start of any async function up to its first await. Write down what it prints.
  2. Keep two lists as you go: microtasks (then, await continuations, queueMicrotask) and timers with their delays.
  3. When the synchronous code ends, run the microtasks in order, adding any new ones to the end of the list, until it is empty.
  4. Take the earliest timer, run it, then empty the microtasks again. Repeat.

Follow-up questions tend to cover why promises beat timers, what await does, what happens if a microtask queues another forever, how to keep a page responsive during heavy work, and, for Node.js roles, process.nextTick and setImmediate.

Event loop questions and answers

What is the JavaScript event loop?

It is the mechanism that decides what JavaScript code runs next. JavaScript runs one piece of code at a time on a single thread. When the call stack is empty, the event loop takes the next task from a queue (a timer, a click, a network response), runs it to completion, then runs every queued microtask, and in a browser gives the page a chance to render. Then it repeats.

Why does a Promise callback run before setTimeout(0)?

Promise callbacks are microtasks and setTimeout callbacks are tasks. After the current task (such as the main script) finishes, the event loop empties the whole microtask queue before it takes the next task, so every .then callback that is ready runs before any timer callback, even one with a delay of 0.

What is the difference between microtasks and macrotasks?

Tasks (often called macrotasks) come from timers, user input, network events and messages; the event loop runs one per turn. Microtasks come from promise reactions, await, queueMicrotask and MutationObserver; the whole microtask queue is emptied after every task and whenever the call stack becomes empty, including microtasks added while emptying it.

Does await block the thread?

No. await pauses only the async function it is in. The rest of the function is scheduled as a microtask for when the awaited promise settles, and control returns to the caller, which keeps running. Code before the first await runs synchronously, as part of the call.

Is setTimeout(fn, 0) really zero milliseconds?

No. It queues fn as a task as soon as possible, which is after the current task and all microtasks. The HTML standard raises the delay to at least 4 ms once timers are nested more than five deep, and browsers slow timers in background tabs to once a second or less.

When does requestAnimationFrame run?

In the rendering step of the event loop, just before the browser paints a frame, typically 60 times a second on a 60 Hz display. All rAF callbacks queued before that frame run together. Browsers skip rendering, and rAF, for hidden tabs.

What is the difference between process.nextTick and setImmediate in Node.js?

process.nextTick callbacks run as soon as the current operation finishes, before promise microtasks and before the event loop moves on. setImmediate callbacks run in the check phase of the event loop, after I/O polling. Despite the names, nextTick runs sooner than setImmediate.

Can microtasks freeze the page?

Yes. Because the event loop empties the microtask queue completely before rendering or taking another task, a microtask that keeps queuing another microtask never lets the loop move on. Clicks are not handled and the page is never painted, just as with an infinite while loop.

Is JavaScript single-threaded?

Each JavaScript environment runs your code on one thread with one call stack, so two callbacks never run at the same time. The browser or Node.js uses other threads for timers, networking and file I/O, and Web Workers or worker_threads give you extra JavaScript threads that communicate by messages.

Further reading

Published Updated