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.
- 1
console.log('script start'); - 2
- 3
setTimeout(() => { - 4
console.log('setTimeout'); - 5
}, 0); - 6
- 7
Promise.resolve() - 8
.then(() => console.log('promise 1')) - 9
.then(() => console.log('promise 2')); - 10
- 11
console.log('script end');
- main script
- empty
- empty
- empty
- nothing printed yet
Running the script
- 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:
- Take one task from a task queue and run it until the call stack is empty.
- Run every microtask in the microtask queue, including ones added along the way.
- In a browser, if it is time for a new frame, run requestAnimationFrame callbacks, update the layout and paint.
- 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
setTimeoutorsetIntervalcallback, once its delay has passed; - an event handler for a click, a key press or a network response;
- a message from
postMessage, aMessageChannelor 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,.catchand.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);MutationObservercallbacks.
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 happens | Call stack | Microtasks | Tasks and timers | Printed so far |
|---|---|---|---|---|---|
| 1 | console.log prints "script start". | main script | – | – | script start |
| 2 | setTimeout 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 |
| 3 | The promise is already resolved, so .then puts its callback straight into the microtask queue. The next .then waits until this callback has returned. | main script | then callback 1 | setTimeout callback (0 ms) | script start |
| 4 | console.log prints "script end". | main script | then callback 1 | setTimeout callback (0 ms) | script start, script end |
| 5 | main 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 1 | setTimeout callback (0 ms) | script start, script end |
| 6 | Microtask checkpoint: then callback 1 is taken from the microtask queue and run. | then callback 1 | – | setTimeout callback (0 ms) | script start, script end |
| 7 | console.log prints "promise 1". | then callback 1 | – | setTimeout callback (0 ms) | script start, script end, promise 1 |
| 8 | then 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 2 | setTimeout callback (0 ms) | script start, script end, promise 1 |
| 9 | Microtask 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 |
| 10 | console.log prints "promise 2". | then callback 2 | – | setTimeout callback (0 ms) | script start, script end, promise 1, promise 2 |
| 11 | then 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 |
| 12 | At 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 |
| 13 | console.log prints "setTimeout". | setTimeout callback (0 ms) | – | – | script start, script end, promise 1, promise 2, setTimeout |
| 14 | setTimeout 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.
setIntervaldoes 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:
- timers: due
setTimeoutandsetIntervalcallbacks; - pending callbacks: some deferred system callbacks, such as certain TCP errors;
- poll: wait for and run I/O callbacks, such as file and network reads;
- check:
setImmediatecallbacks; - 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 call | Browser | Node.js |
|---|---|---|
process.nextTick(fn) | Not available | Before promise microtasks |
Promise.then, await, queueMicrotask | Microtask | Microtask |
setTimeout(fn, 0) | Task when due | Timers phase |
setImmediate(fn) | Not available | Check phase |
requestAnimationFrame(fn) | Rendering step | Not available |
Answers to the programs
The console output of each program in the simulator, which is also what Chromium printed:
| Program | Output, in order |
|---|---|
| setTimeout vs Promise | script start → script end → promise 1 → promise 2 → setTimeout |
| async and await | before → load: start → after → load: after await |
| Two promise chains | A1 → B1 → A2 → B2 → A3 |
| Microtasks between tasks | timeout 1 → microtask from timeout 1 → timeout 2 |
| Timer delays | 0 ms → another 0 ms → 50 ms → 100 ms |
| Microtasks queued by microtasks | micro 1 → micro 2 → micro 3 → timeout |
| requestAnimationFrame | sync → promise → frame → promise in frame → timeout 50 ms |
| Interview puzzle | 1 → 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:
- 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.
- Keep two lists as you go: microtasks (then, await continuations, queueMicrotask) and timers with their delays.
- When the synchronous code ends, run the microtasks in order, adding any new ones to the end of the list, until it is empty.
- 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
- WHATWGHTML Living Standard: Event loopsThe definition browsers implement, including microtask checkpoints and rendering.
- Jake Archibald, 2015Tasks, microtasks, queues and schedulesThe classic walkthrough of the first program on this page.
- MDNUsing microtasks in JavaScript with queueMicrotask()When to queue a microtask and what to watch out for.
- Node.js documentationThe Node.js event loopThe phases, setImmediate and process.nextTick.
- V8 blog, 2018Faster async functions and promisesHow await is implemented and why it takes fewer microtask turns than it used to.
- web.devOptimize long tasksBreaking up work so the event loop can render and respond.
- SemicolonyHow event loops workEvent loops beyond JavaScript: epoll, libuv, and servers that handle many connections on one thread.