How do tasks and microtasks work in the JavaScript event loop?
JavaScript runs one piece of code at a time. After the current code finishes, it runs all waiting microtasks (like promise callbacks) before the next task (like a setTimeout callback).
Explain it simply
Imagine a single cashier at a chai stall. The cashier serves one customer at a time (the call stack). There are two waiting lines. The VIP line is for microtasks, such as promise callbacks. The normal line is for tasks, such as setTimeout callbacks, clicks and network events. Every time the cashier finishes with a customer, they first serve everyone in the VIP line, and only then call the next person from the normal line.
That is why a promise callback usually runs before a setTimeout callback, even when the timeout is zero.
Example
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
// Output: start
// Output: end
// Output: promise
// Output: timeoutThe two normal console.log calls run first because they are part of the current code. Then the microtask queue runs (the promise). Only after that does the event loop take the next task (the timeout).
How it works
- The engine runs the current script until the call stack is empty.
- It drains the microtask queue completely: promise
then/catch/finallycallbacks,queueMicrotask, and the code after anawait. - The browser may render the page.
- It takes one task from the task queue (timer, click, message) and repeats.
Because microtasks are drained completely, a microtask that keeps adding more microtasks can block rendering and freeze the page.
Real-world use
- Knowing why
awaitcode continues "soon" but after the current function. - Debugging why state appears updated in a different order than expected.
- Splitting heavy work with
setTimeoutso the page stays responsive.
Common mistakes
- Believing
setTimeout(fn, 0)runs immediately. It waits for the current code and all microtasks. - Assuming
awaitpauses the whole program. It pauses only that async function; other code keeps running. - Creating endless microtask loops that starve rendering.
What to say in the interview
"JavaScript has a single call stack and an event loop. After each task finishes, the engine drains the whole microtask queue, which holds promise callbacks and code after await, before it picks the next macrotask such as a timer or click. So Promise.resolve().then logs before setTimeout(..., 0). This matters for ordering bugs and for keeping the UI responsive, because long microtask chains can block rendering."
Follow-up questions
- Where does
awaitfit into the queues? - What is
queueMicrotaskand when would you use it? - How is Node's event loop different from the browser's?
Go deeper
Read these once the basics above feel easy.
Node.js adds phases (timers, I/O callbacks, check for setImmediate, close callbacks) and a separate process.nextTick queue that runs even before promise microtasks. In the browser, requestAnimationFrame callbacks run just before painting, which makes them the right place for animation work.