On this page
Core Concurrency Architecture

Event Loop

10 min read•Updated for Node.js 20 LTS
The Node.js event loop allows Node.js to perform non-blocking I/O operations despite JavaScript running on a single thread by offloading operations to the system kernel whenever possible.

JavaScript can do one thing at a time, yet it handles timers, clicks, and network requests without freezing. The mechanism that makes this possible is the Event Loop.

Definition

The Event Loop is a mechanism that continuously checks the call stack and the task queues. When the call stack is empty, it takes the next waiting callback from a queue and pushes it onto the call stack to run.

Analogy: think of a restaurant with one chef. The chef (call stack) cooks one dish at a time. Waiters (browser) take orders and set timers in the background. Finished orders wait in a line (queue). A manager (event loop) keeps watching, and the moment the chef is free, hands over the next order.

JavaScript Alone Has No Timers

The JavaScript engine has only a call stack. It has no built-in timer, no access to the screen, and no network. These superpowers come from the browser, called Web APIs:

Web APIWhat it gives you
setTimeout / setIntervalTimers
DOM APIs (document)Access to the page and its elements
fetchNetwork requests to servers
localStorageStorage inside the browser
consoleLogging to the developer tools
locationThe page URL

The browser gives JavaScript access to all of these through the global object, window. That is why you can write setTimeout(...) instead of window.setTimeout(...).

The Parts of the System

PartWhat it does
Call stackRuns JavaScript code, one context at a time
Web APIsRun timers, listen for events, and make network calls outside the call stack
Callback queue (task queue)Holds callbacks from timers and events that are ready to run
Microtask queueHolds callbacks from promises and MutationObserver, with higher priority
Event loopMoves callbacks from the queues to the call stack when the stack is empty

Example 1: setTimeout

console.log("Start");
setTimeout(function cb() {
console.log("Callback");
}, 5000);
console.log("End");
// Output: Start, End, Callback (after 5 seconds)

Step by step:

  1. The global execution context is pushed onto the call stack.
  2. console.log("Start") prints Start.
  3. setTimeout asks the browser to register cb and start a 5000 ms timer. JavaScript does not wait.
  4. console.log("End") prints End.
  5. The global context is popped off. The call stack is now empty.
  6. When the timer expires, cb is placed in the callback queue.
  7. The event loop sees the empty call stack, moves cb onto it, and cb prints Callback.

Example 2: Event Listeners

console.log("Start");
document.getElementById("btn").addEventListener("click", function cb() {
console.log("Clicked");
});
console.log("End");
  1. addEventListener registers cb in the Web APIs environment and attaches it to the click event.
  2. Start and End print, and the global context is popped off.
  3. The listener stays registered, even though the code has finished.
  4. When the user clicks, cb goes into the callback queue, and the event loop pushes it onto the call stack.

Why do we need a queue at all? If the user clicks 5 times quickly, there will be 5 callbacks waiting. The queue keeps them in order, and the event loop runs them one by one when the stack is free.

Example 3: Promises and the Microtask Queue

Callbacks from promises (.then, .catch, fetch) go into a separate, higher-priority queue: the microtask queue.

console.log("Start");
setTimeout(function cbT() {
console.log("Timeout");
}, 0);
Promise.resolve().then(function cbP() {
console.log("Promise");
});
console.log("End");
// Output: Start, End, Promise, Timeout

Even with a 0 ms timer, Promise prints before Timeout, because the event loop empties the entire microtask queue first, and only then takes one task from the callback queue.

QueueWhat goes inPriority
Microtask queuePromise callbacks (.then, .catch, .finally, fetch), queueMicrotask, MutationObserverHigher
Callback queue (task / macrotask queue)setTimeout, setInterval, DOM eventsLower

Starvation of the Callback Queue

If every microtask creates another microtask, the microtask queue never empties. The callback queue then never gets a chance to run. This is called starvation of the callback queue.

The Event Loop Algorithm

11. Run the code on the call stack until it is empty.
22. Run ALL microtasks, one by one, until the microtask queue is empty.
33. Take ONE task from the callback queue and run it.
44. Repeat from step 2.

Summary

  • JavaScript is single-threaded and has only a call stack.
  • The browser provides Web APIs (setTimeout, DOM, fetch, localStorage, console) through the window object.
  • Web APIs run work in the background and place ready callbacks in a queue.
  • Timer and event callbacks go into the callback queue. Promise and MutationObserver callbacks go into the microtask queue.
  • The event loop moves callbacks to the call stack only when the stack is empty.
  • The microtask queue has higher priority, so it is emptied first.
  • Endless microtasks can starve the callback queue.

Interview Answer: What Is the Event Loop?

JavaScript is single-threaded, so it has just one call stack. Asynchronous work such as timers, DOM events, and network requests is handled by the browser's Web APIs, which JavaScript accesses through the window object.

When an asynchronous operation finishes, its callback is placed in a queue. Callbacks from setTimeout and DOM events go to the callback queue, and callbacks from promises and MutationObserver go to the microtask queue. The event loop continuously checks whether the call stack is empty. When it is, it first runs all microtasks, and then takes one task from the callback queue and pushes it onto the call stack.

Because microtasks have higher priority, a promise callback runs before a setTimeout callback, even with a 0 ms delay. If microtasks keep creating more microtasks, the callback queue can suffer from starvation.

The next topic is the JavaScript engine itself, and what happens inside it when code runs.

Next TopicMicrotasks vs Macrotasks: Predicting Output Order