On this page

Event Loop: Micro vs Macrotasks

4 min read

JavaScript can only do one thing at a time, yet it handles timers, network calls and clicks without freezing. The event loop is how. It also explains the output order of those "predict the output" questions.

Definition

JavaScript runs on a single thread. It has one call stack where code executes. Slow work, like timers and network requests, is handed to the browser. When that work is done, a callback is placed in a queue. The event loop is the loop that moves callbacks from the queues onto the call stack, but only when the stack is empty.

Analogy: a restaurant with one chef. Orders (callbacks) wait in a line. The chef finishes the current dish, then takes the next order. VIP orders (microtasks) always go first.

The Parts

1Call Stack → what is running right now
2Web APIs → browser features: timers, fetch, DOM events (run outside the stack)
3Macrotask queue → callbacks from setTimeout, setInterval, events, I/O
4Microtask queue → callbacks from Promises, queueMicrotask, MutationObserver
5Event loop → moves the next task onto the stack when it is empty

The Order of Work

After each piece of code finishes, the event loop does this:

11. Run all synchronous code until the stack is empty
22. Run ALL microtasks (and any new ones they add) until the microtask queue is empty
33. Maybe render the page
44. Take ONE macrotask, run it
55. Go back to step 2

The key rule: the microtask queue is emptied completely before the next macrotask.

MicrotasksMacrotasks
Sources.then, catch, await, queueMicrotasksetTimeout, setInterval, click events, I/O
PriorityHigherLower
Between tasksRun allRun one

Predict the Output

js
1console.log("1");
2
3setTimeout(() => console.log("2"), 0);
4
5Promise.resolve().then(() => console.log("3"));
6
7console.log("4");

Output: 1 4 3 2.

11. "1" and "4" are synchronous → they print first
22. The timer callback goes to the macrotask queue
33. The promise callback goes to the microtask queue
44. Stack is empty → microtasks run first → "3"
55. Then the macrotask → "2"

A harder one with await:

js
1async function run() {
2 console.log("a");
3 await null;
4 console.log("b"); // runs as a microtask after the current code
5}
6run();
7console.log("c");
8// a c b

Everything after an await is like a .then, so it runs as a microtask.

setTimeout(fn, 0) Is Not Instant

0 means "as soon as possible after the stack and microtasks are clear". Browsers also enforce a small minimum delay for nested timers. A timer is a minimum wait, not a guarantee.

Blocking the Loop

Here is what breaks. A long synchronous task blocks everything, because nothing can move from the queue until the stack is empty.

js
1button.onclick = () => {
2 for (let i = 0; i < 1e9; i++) {} // page freezes, no clicks, no rendering
3};

Fixes: split work into chunks, move it to a Web Worker, or do it on the server.

A microtask that keeps adding microtasks can also starve the page, because rendering waits until the microtask queue is empty.

In React

  • setState updates are batched and applied after your handler finishes. Your handler is a macrotask, so React renders after it.
  • useEffect runs after the browser paints, so it never blocks the first paint. useLayoutEffect runs before paint.
  • A heavy calculation during render blocks the whole UI. That is why useTransition and useDeferredValue exist: they let React split urgent and non-urgent work.
  • await inside an effect or handler resumes as a microtask, after the current code finishes.

Summary

1JS is single threaded → one call stack
2Slow work → Web APIs → callback waits in a queue
3Event loop → moves queued callbacks to the stack when it is empty
4Microtasks (promises, await) → run ALL before the next macrotask
5Macrotasks (timers, events) → run ONE at a time
6Order → sync code, then microtasks, then one macrotask, repeat
7Long sync code → blocks clicks and rendering

Promises sit at the heart of the microtask queue, so they are the next topic.

Next TopicPromises & async/await