Practice JS Problems

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.
  • MacrotaskssetTimeout, 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

1

Synchronous code

All synchronous tasks from the Call Stack execute first.

2

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.

3

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.

4

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, postMessage
  • setImmediate (Node.js)
  • I/O operations (Node.js)

Microtasks:

  • .then(), .catch(), .finally() (Promise)
  • async/await (continuation after await)
  • queueMicrotask()
  • MutationObserver
  • process.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:

  1. console.log("1") — synchronous code, runs immediately
  2. setTimeout — callback goes to macrotask queue
  3. Promise.resolve().then(...) — callback goes to microtask queue
  4. console.log("4") — synchronous code, runs immediately
  5. Call Stack is empty — microtasks run: 3 is printed
  6. Microtasks done — macrotask is taken: 2 is 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:

  1. console.log("3") — synchronous code
  2. foo() is called: console.log("1") runs synchronously
  3. await Promise.resolve() pauses foo. Everything after await becomes a microtask
  4. console.log("4") — synchronous code continues
  5. 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.

Practice JS Problems