- JavaScript
- Event Loop
- Async
- Interview Preparation
- Web Development
JavaScript Event Loop Explained: Call Stack, Microtasks and Macrotasks (with Output Questions)
The JavaScript event loop runs your synchronous code on one call stack, and when the stack is empty it first runs every queued microtask (Promise callbacks, code after await), then lets the browser render, then runs one macrotask (a setTimeout callback, an event, I/O) and repeats. That is why a Promise callback always runs before a setTimeout(fn, 0) callback.
What is the event loop in JavaScript?
JavaScript runs on a single thread with a single call stack: one function runs at a time, to completion. Yet a page can wait for a network response, run a timer and react to clicks at the same time. The event loop is what makes that work.
The pieces involved:
| Piece | What it does |
|---|---|
| Call stack | Runs JavaScript, one frame at a time. Synchronous code lives here. |
| Web APIs / Node APIs | The browser or Node.js handles timers, fetch, DOM events and file I/O outside the stack. |
| Macrotask queue | Callbacks from timers, events, I/O and postMessage wait here. The HTML spec calls these tasks. |
| Microtask queue | Promise reactions, code after await, queueMicrotask and MutationObserver callbacks wait here. |
| Event loop | Moves callbacks from the queues to the stack when the stack is empty. |
When you call setTimeout(cb, 1000), JavaScript does not wait. The browser starts the timer, your code moves on, and after about a second cb is put into the macrotask queue. It only runs once the call stack is empty and the event loop picks it.
How does one turn of the event loop work?
Each turn follows the same steps:
- Take the oldest macrotask from a task queue and run it to completion. The initial script counts as the first macrotask.
- Run every microtask in the microtask queue, including microtasks added while doing this, until the queue is empty.
- If it is time to paint, update the rendering: run
requestAnimationFramecallbacks, recalculate styles and layout, and paint. - Go back to step 1.
Microtask vs macrotask: what goes where?
| Microtasks (high priority) | Macrotasks (one per turn) |
|---|---|
Promise.then, catch, finally | setTimeout, setInterval |
Code after await in an async function | UI events: click, keydown, scroll |
queueMicrotask(fn) | Network and file I/O callbacks |
MutationObserver callbacks | MessageChannel and postMessage |
process.nextTick (Node.js, its own queue that runs first) | setImmediate (Node.js) |
Note that the Promise constructor's executor function runs synchronously. Only the then callbacks are microtasks.
Output question 1: setTimeout vs Promise order
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Output: A, D, C, B
AandDare synchronous and print first.- The
thencallback is a microtask, soCruns as soon as the script ends. - The timer callback is a macrotask, so
Bwaits for the next turn, even with a 0 ms delay.
This is Q18 in the Top 50 JavaScript interview questions, and it is asked in almost every frontend interview.
Output question 2: how does async/await fit in?
async function first() {
console.log("1");
await second();
console.log("2");
}
async function second() {
console.log("3");
}
console.log("4");
setTimeout(() => console.log("5"), 0);
first();
Promise.resolve().then(() => console.log("6"));
console.log("7");
Output: 4, 1, 3, 7, 2, 6, 5
- An async function runs synchronously until its first
await. Sofirst()prints1, callssecond()which prints3, and then pauses. - Everything after
awaitbecomes a microtask. Becausesecond()has already returned a resolved promise, the continuation that prints2is queued right away, before thethenthat prints6. 7is synchronous. Then the microtasks run in order:2,6. The timer5comes last.
Output question 3: Promises inside setTimeout
setTimeout(() => {
console.log("A");
Promise.resolve().then(() => console.log("B"));
}, 0);
setTimeout(() => console.log("C"), 0);
Promise.resolve().then(() => {
console.log("D");
setTimeout(() => console.log("E"), 0);
});
Output: D, A, B, C, E
- After the script, the microtask prints
Dand schedules a third timerEat the back of the queue. - The first timer runs and prints
A, and queues a microtask. - That microtask (
B) runs before the second timer, because microtasks are drained after every macrotask. - Then
C, thenE.
A common wrong answer is D, A, C, B, E. Browsers, and Node.js since version 11, both give D, A, B, C, E.
Output question 4: chained Promises and queueMicrotask
Promise.resolve()
.then(() => console.log(1))
.then(() => console.log(2));
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4));
queueMicrotask(() => console.log(5));
Output: 1, 3, 5, 2, 4
Only the first then of each chain is queued at the start, together with the queueMicrotask callback: 1, 3, 5. A chained then is queued only when the previous one finishes, so 2 and 4 join the back of the queue afterwards. Chains interleave; they do not run one after another.
Output question 5: why doesn't the red ever show?
button.addEventListener("click", () => {
box.style.background = "red";
const end = Date.now() + 2000;
while (Date.now() < end) {} // busy-wait 2 seconds
box.style.background = "blue";
});
What the user sees: the page freezes for 2 seconds, then the box turns blue. Red never appears.
Rendering only happens between macrotasks. The click handler is one macrotask, so the browser cannot paint the red background until the handler returns, and by then the colour is already blue. The same thing happens if you queue microtasks forever: the microtask queue never empties, so the browser never gets to render.
Output question 6: process.nextTick vs Promise vs setImmediate in Node.js
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
console.log("sync");
Output in a CommonJS file: sync, nextTick, promise, then timeout and immediate in either order.
- Node.js runs the
process.nextTickqueue before the Promise microtask queue. - Node's event loop has phases: timers (
setTimeout), poll (I/O), check (setImmediate) and close. Whethertimeoutorimmediateprints first in the main module depends on how fast the process starts, so the order is not guaranteed. Inside an I/O callback,setImmediatealways runs first. - In an ES module (
.mjsor"type": "module"), the module itself runs inside a Promise job, sopromisecan print beforenextTick. Interviewers usually mean CommonJS.
The Node.js handwritten notes cover the event loop phases in more detail.
What are the common event loop mistakes?
- Thinking
setTimeout(fn, 0)runs immediately. It runs after the current script and all microtasks, and browsers clamp nested timers to at least 4 ms after five levels of nesting. - Thinking the delay is exact. The delay is a minimum. A busy call stack pushes the callback later.
- Forgetting the Promise executor is synchronous. In
new Promise(r => { console.log("x"); r(); }),xprints immediately. - Blocking the main thread. Long loops, heavy JSON parsing or big sorts freeze clicks, timers and painting. Split the work, or move it to a Web Worker.
- Using
awaitin a loop when the calls are independent. Each iteration waits for the previous one. Start them together and usePromise.allinstead. - Infinite microtasks. A
thenthat keeps queuing anotherthenstarves rendering and timers, just like awhile (true)loop.
How do you explain the event loop in an interview?
Keep it to four sentences, then offer an example:
- JavaScript is single-threaded and runs code on one call stack.
- Slow work such as timers, network and events is handed to the browser or Node.js, and the callbacks wait in queues.
- When the stack is empty, the event loop runs all microtasks (Promise callbacks), lets the browser render, and then runs one macrotask (a timer or event).
- That is why
Promise.thenruns beforesetTimeout(fn, 0).
Then write output question 1 on the board and walk through it. If the interviewer pushes further, mention that await splits a function into a microtask, and that Node.js adds process.nextTick and setImmediate.
Key takeaway
Synchronous code first, then every microtask, then render, then one macrotask, and repeat. Promise callbacks and code after await are microtasks; timers, events and I/O are macrotasks. With that one rule you can answer almost any event loop output question. To practise the rest of the topics that come up with it, such as closures, this, hoisting and Promise polyfills, work through the free Top 50 JavaScript interview questions PDF, and revise the basics from the JavaScript handwritten notes.
Frequently asked questions
What is the event loop in JavaScript?
The event loop is the mechanism that lets single-threaded JavaScript handle asynchronous work. JavaScript runs code on one call stack; timers, network requests and events are handled by the browser or Node.js, and their callbacks wait in queues. Whenever the call stack is empty, the event loop runs all pending microtasks, lets the browser render if needed, and then runs the next macrotask.
What is the difference between a microtask and a macrotask?
Microtasks are Promise callbacks (then, catch, finally), the code after await, queueMicrotask and MutationObserver callbacks. Macrotasks (called tasks in the HTML spec) are setTimeout, setInterval, I/O, UI events and message events. After every macrotask the event loop empties the whole microtask queue, including microtasks added along the way, before it takes the next macrotask.
Why does a Promise callback run before setTimeout with 0 delay?
Because a resolved Promise's then callback is a microtask and setTimeout's callback is a macrotask. When the current script finishes, the event loop drains the microtask queue first, so the Promise callback runs before the timer callback, even if the timer was created earlier and has a delay of 0.
Is JavaScript single-threaded?
The JavaScript engine runs your code on a single main thread with one call stack, so only one piece of JavaScript runs at a time. The browser and Node.js use other threads for timers, networking and file I/O, and Web Workers or worker_threads can run separate JavaScript threads that communicate by messages.
What is the order of process.nextTick, Promise and setImmediate in Node.js?
In a CommonJS script, synchronous code runs first, then process.nextTick callbacks, then Promise microtasks, then macrotasks such as setTimeout and setImmediate. The order between setTimeout 0 and setImmediate in the main module is not guaranteed, but inside an I/O callback setImmediate always runs first. In ES modules, Promise callbacks can run before nextTick callbacks.