Event Loop
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 API | What it gives you |
|---|---|
setTimeout / setInterval | Timers |
DOM APIs (document) | Access to the page and its elements |
fetch | Network requests to servers |
localStorage | Storage inside the browser |
console | Logging to the developer tools |
location | The 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
| Part | What it does |
|---|---|
| Call stack | Runs JavaScript code, one context at a time |
| Web APIs | Run 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 queue | Holds callbacks from promises and MutationObserver, with higher priority |
| Event loop | Moves 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:
- The global execution context is pushed onto the call stack.
console.log("Start")printsStart.setTimeoutasks the browser to registercband start a 5000 ms timer. JavaScript does not wait.console.log("End")printsEnd.- The global context is popped off. The call stack is now empty.
- When the timer expires,
cbis placed in the callback queue. - The event loop sees the empty call stack, moves
cbonto it, andcbprintsCallback.
Example 2: Event Listeners
console.log("Start");document.getElementById("btn").addEventListener("click", function cb() {console.log("Clicked");});console.log("End");
addEventListenerregisterscbin the Web APIs environment and attaches it to the click event.StartandEndprint, and the global context is popped off.- The listener stays registered, even though the code has finished.
- When the user clicks,
cbgoes 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.
| Queue | What goes in | Priority |
|---|---|---|
| Microtask queue | Promise callbacks (.then, .catch, .finally, fetch), queueMicrotask, MutationObserver | Higher |
| Callback queue (task / macrotask queue) | setTimeout, setInterval, DOM events | Lower |
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 thewindowobject. - 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.