JavaScript Event Loop Explained: Microtasks, Macrotasks, Promises, and Interview Output Questions
The JavaScript event loop is one of the highest-value topics for frontend, backend, and full stack interviews. It explains why JavaScript can be single-threaded and still handle timers, API callbacks, user events, Promise chains, async/await, and UI rendering without freezing every time work is scheduled.
Most developers memorize small output questions, but ranking and interview performance both improve when the explanation is deeper. This article builds the mental model from the call stack to microtasks and macrotasks, then connects it to browser rendering and Node.js behavior.
Search intent this article targets
This post targets practical long-tail searches such as "JavaScript event loop interview questions", "microtasks vs macrotasks JavaScript", "Promise setTimeout output question", and "async await event loop explained". These searches come from developers preparing for interviews and debugging real async behavior.
- Students who can write async code but cannot explain execution order.
- Frontend developers debugging delayed rendering or blocked clicks.
- Node.js developers comparing timers, promises, and I/O callbacks.
- Interview candidates practicing output prediction questions.
The core mental model
JavaScript runs synchronous code on the call stack. When the stack is busy, nothing else runs. Async APIs do not magically run your callback in the middle of the current function. They schedule work for later. The event loop decides when the queued work can return to the stack.
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// Output:
// A
// D
// C
// BThe synchronous logs run first. Promise callbacks run in the microtask queue. setTimeout callbacks run as macrotasks. Microtasks are drained before the next macrotask is picked.
Call stack, Web APIs, and queues
In the browser, APIs such as setTimeout, DOM events, fetch, and requestAnimationFrame are provided by the environment around JavaScript. The JavaScript engine handles the stack. The browser handles scheduling and pushes callbacks into queues when they are ready.
- Call stack: where currently executing JavaScript frames live.
- Web APIs: browser-managed async capabilities like timers, DOM events, and network work.
- Macrotask queue: timers, UI events, and other task-level callbacks.
- Microtask queue: Promise callbacks, queueMicrotask, and MutationObserver callbacks.
- Rendering step: the browser may update layout and paint between tasks after microtasks are drained.
Why microtasks run before timers
After synchronous code completes and the call stack becomes empty, the event loop drains the microtask queue before moving to the next macrotask. That is why Promise callbacks usually run before setTimeout(..., 0), even when the timer delay is zero.
setTimeout(() => console.log('timer 1'), 0);
Promise.resolve()
.then(() => {
console.log('promise 1');
return Promise.resolve();
})
.then(() => console.log('promise 2'));
setTimeout(() => console.log('timer 2'), 0);
// promise 1
// promise 2
// timer 1
// timer 2A zero-delay timer does not mean immediate execution. It means the callback becomes eligible for a future task turn after the current synchronous work and microtasks finish.
Async await is Promise syntax, not a new queue
await pauses the async function and schedules the continuation through Promise machinery. That continuation behaves like a microtask. This is why async/await output questions are really Promise output questions with cleaner syntax.
async function run() {
console.log('inside start');
await null;
console.log('inside after await');
}
console.log('script start');
run();
console.log('script end');
// script start
// inside start
// script end
// inside after awaitThe code before await runs synchronously. The code after await resumes later, after the current stack clears.
Browser rendering and long tasks
The event loop also explains why heavy synchronous JavaScript hurts user experience. If a task runs for a long time, the browser cannot handle clicks, scrolls, keyboard input, or paint updates until the stack clears and microtasks are drained.
button.addEventListener('click', () => {
const start = performance.now();
while (performance.now() - start < 2000) {
// Blocks the main thread for 2 seconds.
}
console.log('done');
});Performance work is often about splitting long tasks, moving expensive work off the main thread, reducing JavaScript bundle cost, and letting the browser paint at the right time.
Node.js differences you should mention
Node.js also has an event loop, but its phases are tied to libuv and backend concerns such as timers, pending callbacks, poll, check, and close callbacks. Browser rendering does not exist in Node.js, but I/O scheduling does.
- process.nextTick has special priority in Node.js and can run before Promise microtasks.
- setImmediate is associated with the check phase.
- setTimeout(..., 0) and setImmediate ordering can depend on whether the code runs inside an I/O callback.
- CPU-heavy synchronous work blocks Node.js from handling other requests on that process.
Interview explanation
A strong interview answer: JavaScript executes synchronous code on the call stack. Async APIs schedule callbacks. When the stack is empty, the event loop drains microtasks such as Promise callbacks before moving to macrotasks such as timers. In browsers, rendering can happen between task turns after microtasks finish. In Node.js, libuv phases and process.nextTick add runtime-specific behavior.
Quick FAQ
Are Promise callbacks microtasks?
Yes. Promise reactions run as microtasks, which are processed after the current synchronous stack and before the next macrotask.
Does setTimeout with zero milliseconds run immediately?
No. It schedules a timer callback for a future task turn. Synchronous code and pending microtasks run first.
Why does the UI freeze during heavy JavaScript?
Because the main thread is busy running a long task. The browser cannot process input or paint smoothly until the stack clears.
