Event Loop Deep Dive. Microtasks vs Macrotasks for Interviews
Event Loop is a mechanism in JavaScript that manages asynchronous tasks and event queues. It allows JavaScript to work in a single-threaded model, processing asynchronous operations without blocking the main execution thread.
How Does Event Loop Work?
Event Loop is an infinite loop that executes event handlers. During its operation, the browser distributes tasks into two main queues:
- Microtasks — promises (
.then,.catch,.finally),queueMicrotask(),MutationObserver. - Macrotasks —
setTimeout,setInterval, DOM events (click,input),MessageChannel,requestAnimationFrame.
fetch is not a macrotask:
A common interview mistake: treating fetch as a macrotask. In reality, fetch() returns a Promise. The network request runs in Web API, but when the response arrives, the .then() callback goes into the microtask queue, not the macrotask queue. Remember: everything that works through Promises (fetch, async/await) is processed as a microtask.
Main Execution Order
Synchronous code
All synchronous tasks from the Call Stack execute first.
Microtask queue
After synchronous code completes, all microtasks execute. Every microtask runs before the browser picks up the next macrotask. If a microtask adds a new microtask, it also runs in the same cycle.
Rendering (if needed)
The browser checks if a repaint is needed. If there are DOM or style changes, it runs: requestAnimationFrame callbacks, style recalculation, layout, paint.
One macrotask
One macrotask is taken from the queue and executed. Then the cycle repeats from step 2.
Data Structures
Call Stack (LIFO — Last In, First Out) — contains all currently executing functions. When a function is called, it is added to the stack. After execution, it is removed. If another function is called inside, it is pushed on top of the stack.
Web API — asynchronous operations (setTimeout, event handlers, network requests) are passed to Web API. This is an environment provided by the browser (or Node.js). After the async operation completes, its callback is placed in the appropriate queue.
Task Queue / Callback Queue (FIFO — First In, First Out) — stores macrotask callbacks. Event Loop moves tasks from this queue to the Call Stack when the stack is empty.
Microtask Queue — a separate queue for microtasks. Has priority over the Task Queue. Fully drained after every synchronous task or macrotask.
Microtasks vs. Macrotasks
Macrotasks:
setTimeout,setInterval- DOM events (
click,input,load) MessageChannel,postMessagesetImmediate(Node.js)- I/O operations (Node.js)
Microtasks:
.then(),.catch(),.finally()(Promise)async/await(continuation after await)queueMicrotask()MutationObserverprocess.nextTick()(Node.js, higher priority than other microtasks)
Basic Example
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Result: 1, 4, 3, 2
Breakdown:
console.log("1")— synchronous code, runs immediatelysetTimeout— callback goes to macrotask queuePromise.resolve().then(...)— callback goes to microtask queueconsole.log("4")— synchronous code, runs immediately- Call Stack is empty — microtasks run:
3is printed - Microtasks done — macrotask is taken:
2is printed
Important:
Microtasks have priority over macrotasks. Even if setTimeout is called with delay 0, its callback will only run after all microtasks.
Complex Example (common in interviews)
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
}, 0);
Promise.resolve().then(() => {
console.log("4");
setTimeout(() => console.log("5"), 0);
});
setTimeout(() => console.log("6"), 0);
Promise.resolve().then(() => console.log("7"));
console.log("8");
Result: 1, 8, 4, 7, 2, 3, 6, 5
Step-by-step breakdown:
Synchronous code: 1 and 8 are printed.
Microtasks (first cycle): 4 is printed (first Promise), then 7 (second Promise). Inside the callback for 4, a setTimeout with console.log("5") is created and goes to the macrotask queue.
First macrotask: setTimeout with console.log("2"). Prints 2. Inside it, a Promise with console.log("3") is created — this is a microtask, runs immediately: prints 3.
Second macrotask: setTimeout with console.log("6"). Prints 6.
Third macrotask: setTimeout with console.log("5") (created inside the microtask). Prints 5.
requestAnimationFrame
requestAnimationFrame (rAF) occupies a special place in the Event Loop. It runs after microtasks but before macrotasks, right before rendering.
setTimeout(() => console.log("setTimeout"), 0);
requestAnimationFrame(() => console.log("rAF"));
Promise.resolve().then(() => console.log("Promise"));
console.log("sync");
Expected result: sync, Promise, rAF, setTimeout
In practice, the order of rAF and setTimeout may vary across browsers, but rAF always runs before the next render cycle. Use requestAnimationFrame for animations and DOM manipulation.
async/await and Event Loop
Interviewers often ask how async/await works in the context of the Event Loop. Code after await becomes a callback inside .then(), meaning it goes into the microtask queue.
async function foo() {
console.log("1");
await Promise.resolve();
console.log("2");
}
console.log("3");
foo();
console.log("4");
Result: 3, 1, 4, 2
Breakdown:
console.log("3")— synchronous codefoo()is called:console.log("1")runs synchronouslyawait Promise.resolve()pausesfoo. Everything after await becomes a microtaskconsole.log("4")— synchronous code continues- Call Stack is empty — microtask runs:
console.log("2")
The key point: await does not block the main thread. It "splits" the function into two parts: before await (synchronous) and after await (microtask).
queueMicrotask
queueMicrotask() allows you to explicitly add a task to the microtask queue. Useful when you need to defer code execution but run it before any macrotasks.
console.log("1");
queueMicrotask(() => console.log("2"));
setTimeout(() => console.log("3"), 0);
queueMicrotask(() => console.log("4"));
console.log("5");
Result: 1, 5, 2, 4, 3
queueMicrotask has the same priority as .then(). Both microtasks (2 and 4) run before the setTimeout macrotask (3).
Macrotask Starvation
If microtasks keep adding new microtasks indefinitely, macrotasks and rendering will never execute. This is called starvation.
function recursiveMicrotask() {
Promise.resolve().then(() => {
console.log("microtask");
recursiveMicrotask();
});
}
recursiveMicrotask();
setTimeout(() => console.log("this will never run"), 0);
The browser will freeze because the microtask queue never empties. Rendering is blocked, setTimeout will never execute. In an interview you might be asked: "Can a Promise block rendering?" The answer is yes, if microtasks keep adding new microtasks indefinitely.