LoopLens

Microtasks vs macrotasks in JavaScript

JavaScript has two main waiting lines for callbacks. Microtasks (promise callbacks, code after await, queueMicrotask) run as soon as the current code finishes, and all of them run before anything else. Macrotasks, simply called tasks in the HTML standard (timers, events, messages), run one at a time, with every microtask drained in between.

Try it

Here a timer callback creates a promise callback. Press Next step and watch the microtask run before the second timer, even though that timer was already waiting in the task queue.

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 19 · 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: setTimeout(() => {
2 console.log("task 1");
3 Promise.resolve().then(() => console.log("microtask from task 1"));
4}, 0);
5
6setTimeout(() => console.log("task 2"), 0);

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

setTimeout(() => {
  console.log("task 1");
  Promise.resolve().then(() => console.log("microtask from task 1"));
}, 0);

setTimeout(() => console.log("task 2"), 0);
  1. The script schedules two timers and finishes. Both callbacks move to the task queue.
  2. The event loop takes the first task. It prints task 1 and creates a resolved promise, whose .then callback joins the microtask queue.
  3. Task 1 finishes. Before taking another task, the event loop empties the microtask queue, so microtask from task 1 prints.
  4. Only now does the second task run and print task 2.

Which callbacks go where

Microtask queueTask queue (macrotasks)
.then, .catch, .finally callbackssetTimeout and setInterval callbacks
the rest of an async function after awaituser events such as clicks and key presses
queueMicrotask(fn)postMessage and MessageChannel messages
MutationObserver callbacks (browsers)network and file I/O callbacks; the main script itself

requestAnimationFrame callbacks are neither: browsers run them just before drawing the next frame. Node.js adds its own queues, such as process.nextTick (which runs even before promise callbacks) and setImmediate.

The rule: one task, then all microtasks

run one task
  → run microtasks until the microtask queue is empty
    (including microtasks added by other microtasks)
→ (browser) maybe update the screen
→ next task

This one rule explains most "what prints first?" puzzles. A small one:

setTimeout(() => console.log("task"), 0);
queueMicrotask(() => console.log("microtask"));
console.log("sync");

The output is sync, microtask, task: the script is the current task, so it finishes first; the microtask runs as soon as the stack is empty; the timer has to wait for the next turn of the event loop.

Microtasks that add microtasks

Promise.resolve()
  .then(() => console.log("A1"))
  .then(() => console.log("A2"));

Promise.resolve()
  .then(() => console.log("B1"))
  .then(() => console.log("B2"));

This prints A1, B1, A2, B2. Each .then callback is only queued once the promise before it is resolved. So A2 joins the queue after A1 has run, behind B1, which was already waiting. The microtask queue keeps running until it is empty, so both chains finish before any task gets a turn.

Microtasks can starve everything else

// Don't run this: the page never gets a chance to update.
function forever() {
  queueMicrotask(forever);
}
forever();

Because the event loop only moves on when the microtask queue is empty, a microtask that always schedules another one blocks timers, clicks and screen updates for good. A timer-based loop (setTimeout(forever, 0)) would not freeze the page, because every timer callback is a separate task.

Common mistakes

  • "setTimeout(fn, 0) and a resolved promise run at the same time." The promise callback always runs first, because microtasks are drained before the next task.
  • "Only one microtask runs between tasks." All of them run, including new ones added along the way.
  • Assuming Node.js behaves exactly like a browser. The promise rules are the same, but Node.js has extra queues and phases.

What LoopLens simulates

LoopLens models the task queue, the microtask queue and the rule between them, and its examples produce the same output order as real JavaScript (they are checked against Node.js). It supports promises, await, queueMicrotask and timers. DOM events, messages, MutationObserver, requestAnimationFrame, screen updates and Node.js-only queues are not simulated. Learn how promises create these microtasks in the promises guide.

Next: How JavaScript promises work →

Open the visualizer with your own code