JavaScript event loop: microtasks, macrotasks, and the call stack
Trace how promises, timers, and fetch callbacks move through microtask and task queues so async logs stop looking random in interviews and production bugs.
This article is published in English.
Sources: Event Loop in JavaScript — JavaScript Interview Questions & Answers — Part 6
Understanding the event loop is central to writing reliable JavaScript. The runtime interleaves synchronous work on the call stack with asynchronous callbacks that arrive later through host APIs.
The event loop schedules those asynchronous callbacks once the stack is clear.
console.log("start");
setTimeout(function (){
console.log("timer");
}, 5000);
fetch("https://api.netflix.com")
.then(() => {
console.log("netflix");
});
console.log("end");
console.log("start") runs immediately on the stack, so start prints first.
setTimeout registers its callback with the host (browser Web API or Node timer subsystem) and starts the delay. JavaScript does not pause for five seconds; it continues.
fetch likewise hands the network request to the host. The engine does not wait for the response before moving on. end therefore prints while timers and network work are still outstanding.
If the server answers after about one second, the promise fulfillment callback becomes a microtask and lands in the microtask queue. Microtasks outrank macrotasks (the ordinary callback/task queue). After synchronous code finishes and the call stack is empty, the event loop drains microtasks before taking the next macrotask. So the fetch handler can print before the five-second timer fires.
When the timeout expires, its callback enters the task queue. Only when the stack is empty and the microtask queue is empty does the event loop move that callback onto the stack, where it prints timer.
Microtask queue versus callback queue
Microtasks have higher priority than the callback (task) queue. Promise reactions (.then, .catch, .finally) and MutationObserver callbacks are microtasks. setTimeout, setInterval, and many DOM event listeners enqueue macrotasks.
Whenever the stack empties, the loop processes all pending microtasks before pulling one macrotask. Three queued microtasks run back-to-back before the next timer or click handler.
When does the event loop start?
It does not “boot” after your script. The loop is continuous in the runtime: repeatedly checking whether the stack is empty and whether queues hold work.
Do only asynchronous host callbacks use the host environment?
Yes for the queueing path. Asynchronous operations go through host APIs and queues. Synchronous callbacks passed to map, filter, or reduce run directly on the stack and never register with the Web API environment as deferred work.
Blocking the main thread
All JavaScript on a page (or a Node worker’s main thread) shares one call stack. A long synchronous function x() monopolizes that stack: nothing else—renders, clicks, other JS—runs until it returns. Prefer asynchronous APIs for waiting work: timers, network, and file I/O in Node, so the stack stays free for other tasks.
Web APIs
setTimeout, DOM helpers such as document.getElementById, fetch, localStorage, and even console are not core language features. Hosts expose them alongside the JS engine. In browsers they hang off the global window object. Knowing the boundary—language versus host—clarifies why the event loop and queues exist: to reconnect host completions to the single-threaded engine safely.
Interview follow-ups often ask what prints when a Promise.resolve().then(...) races a setTimeout(..., 0). The promise callback wins because microtasks drain fully before the next macrotask, even when the timer delay is zero. That single rule explains a large share of “why did this log out of order?” bugs in async UI code.