Back to JavaScript
2026-02-239 min read

Node Event Loop (JavaScript)

Learn Node Event Loop (JavaScript) step by step with clear examples and exercises.

Why This Matters

Node.js is a powerful JavaScript runtime built on Chrome's V8 JavaScript engine, which allows developers to run JavaScript code outside of a web browser. One of its unique features is the event loop, which handles asynchronous operations efficiently. Understanding this crucial concept is essential for writing scalable and maintainable Node.js applications.

In a single-threaded environment like JavaScript, handling multiple tasks concurrently can be challenging. Asynchronous programming is the solution, but managing these tasks can lead to callback hell. The Node.js event loop helps manage asynchronous operations efficiently and makes it easier to write scalable and maintainable code.

Prerequisites

Before diving into the Node event loop, you should have a good understanding of:

  • JavaScript basics (variables, functions, loops, control structures)
  • Callbacks and Promises in JavaScript
  • Asynchronous programming concepts
  • Understanding the difference between synchronous and asynchronous code
  • Familiarity with Node.js core modules and packages like fs, http, and events

Additional Resources

If you need to brush up on any of these topics, here are some resources that may help:

Core Concept

The Node.js event loop is responsible for managing asynchronous tasks in a single-threaded environment. It consists of three main components: the call stack, the task queue (also known as the callback queue), and the I/O polling layer.

  1. Call Stack: This is where JavaScript executes synchronous code. When you run a script, it starts at the top of the call stack and works its way down until all functions are executed. The call stack ensures that function calls are properly nested and executed in the correct order.
  1. Task Queue (Callback Queue): This queue holds asynchronous tasks that need to be processed but cannot be executed because the call stack is already full. These tasks include callbacks from I/O operations, timers, and other asynchronous functions. When a task is added to the task queue, it waits until the call stack is empty before being processed.
  1. I/O Polling Layer: This layer is responsible for handling I/O operations and timers. It notifies the event loop when an I/O operation or timer is ready, which moves the task to the task queue. The polling layer uses non-blocking I/O calls, allowing Node.js to handle multiple I/O operations concurrently without blocking the main thread.

The event loop continuously checks the call stack and the task queue. When the call stack is empty, it takes a task from the task queue and pushes it onto the call stack to be executed. This process continues until all tasks are processed or new tasks are added to the task queue. The event loop's efficient management of asynchronous tasks allows Node.js to handle multiple operations concurrently without blocking the main thread, improving the performance of your applications.

Event Loop Phases

The event loop follows a specific order when processing tasks:

  1. Timers (setTimeout, setInterval)
  2. Pending I/O callbacks (networking, filesystem operations)
  3. Idle, prepare, and poll callbacks (timer-related tasks like process.nextTick())
  4. Poll phase (check for more network connections, timers, and I/O callbacks)
  5. Check if the call stack is empty; if not, continue to step 3

Event Loop vs. Web APIs

Note that that the event loop in Node.js is different from web APIs like setTimeout and setInterval. In a browser environment, these functions are part of the web APIs and not directly related to the event loop. However, in Node.js, they are handled by the event loop as part of the I/O polling layer.

Worked Example

Let's create an example to illustrate how the event loop works in Node.js:

console.log('Start');
setTimeout(function () {
console.log('Timeout');
}, 0);

for (let i = 0; i < 1e6; i++) {
// empty loop to delay execution
}

console.log('End');

In this example, we have synchronous code (console.log('Start')) and an asynchronous operation using setTimeout. The event loop handles the asynchronous task by moving it to the task queue when the call stack is empty. The loop at the end delays execution to demonstrate the event loop's behavior.

When you run this script, you will see the following output:

Start
End
Timeout

This demonstrates that the event loop handles asynchronous tasks after synchronous code and continues executing them even when new synchronous code is added to the call stack.

Understanding the Order of Execution

To better understand the order of execution, let's add a few more lines:

console.log('Start');
setImmediate(function () {
console.log('Immediate');
});

for (let i = 0; i < 1e6; i++) {
// empty loop to delay execution
}

process.nextTick(function () {
console.log('Next Tick');
});

console.log('End');
setTimeout(function () {
console.log('Timeout');
}, 0);

In this example, we added setImmediate and process.nextTick to the task queue. The output will be:

Start
Next Tick
End
Immediate
Timeout

This shows that process.nextTick is executed before any I/O callbacks (in this case, setImmediate) and timers (setTimeout).

Common Mistakes

  1. Ignoring callbacks: Neglecting to handle callback functions can lead to unhandledPromiseRejections or memory leaks. Always ensure that your callbacks are properly handled and that errors are propagated up the call stack using throw or by returning a rejected Promise.
  1. Blocking the event loop: Performing heavy computations or long-running tasks in the main thread can block the event loop, causing other tasks to pile up in the task queue. Use asynchronous functions like setTimeout, Promises, or streams to handle these operations and avoid blocking the main thread.
  1. Misusing callbacks: Callbacks should be used for handling asynchronous results and not for synchronous control flow. Misuse of callbacks can lead to callback hell, making your code difficult to read and maintain. To simplify the control flow, use Promises or async/await to manage asynchronous operations more effectively.
  1. Not using proper error handling: Proper error handling is crucial when working with asynchronous code. Always ensure that errors are caught and handled appropriately, either by using try-catch blocks or by propagating them up the call stack using throw or by returning a rejected Promise.
  1. Misusing Promises: Promises can be misused by forgetting to resolve or reject them properly, leading to unresolved Promises that pile up in the task queue. Always ensure that your Promises are resolved or rejected correctly and handle errors appropriately.

Practice Questions

  1. Write a Node.js script that uses setInterval to log a message every second and prints "Done" after 5 seconds.
let counter = 0;
const intervalId = setInterval(() => {
console.log(`Second ${counter++}`);
if (counter === 30) {
clearInterval(intervalId);
console.log('Done');
}
}, 1000);
  1. Implement a simple HTTP server using Node.js that listens on port 3000 and responds with "Hello World!" for any request.
const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World!\n');
});

server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
  1. Write a script that reads a file asynchronously and logs its contents to the console using Node.js's built-in fs module.
const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});

FAQ

  1. What is the purpose of the event loop in Node.js? The event loop manages asynchronous tasks in a single-threaded environment, allowing Node.js to handle multiple operations concurrently without blocking the main thread. This improves the performance of your applications by ensuring that I/O operations and timers don't block the execution of synchronous code.
  1. How does the event loop work? The event loop consists of the call stack, task queue (callback queue), and I/O polling layer. It continuously checks the call stack and the task queue. When the call stack is empty, it takes a task from the task queue and pushes it onto the call stack to be executed. This process continues until all tasks are processed or new tasks are added to the task queue.
  1. What happens if the call stack is full? If the call stack is full, new tasks are added to the task queue until there's room in the call stack. This ensures that asynchronous operations don't block synchronous code execution. When a function on the call stack completes, it pops off the call stack, and if the task queue is not empty, the next task is moved to the call stack to be executed.
  1. How can I handle long-running tasks without blocking the event loop? Use asynchronous functions like setTimeout, Promises, or streams to handle long-running tasks and avoid blocking the main thread. These functions allow Node.js to continue processing other tasks while the long-running task is being executed in the background.
  1. What is callback hell, and how can it be avoided? Callback hell refers to deeply nested callbacks that make code difficult to read and maintain. To avoid callback hell, use Promises or async/await to manage asynchronous operations more effectively. These tools simplify the control flow by allowing you to write asynchronous code using a synchronous-like syntax, making it easier to understand and debug your code.
  1. What is the difference between setImmediate and setTimeout? Both setImmediate and setTimeout are used for scheduling tasks in Node.js, but they have some key differences:
  • setImmediate schedules a task to be executed as soon as the current poll phase ends (during the next I/O loop iteration). This means that setImmediate tasks are processed before timers and other I/O callbacks.
  • setTimeout schedules a task to be executed after a specified delay. The delay is in milliseconds, and the task will be added to the task queue when the delay expires. Timers are processed during the poll phase, which means that they can be delayed if there are other I/O callbacks or timers ahead of them in the event loop.
  1. What is the purpose of process.nextTick? process.nextTick is used to schedule a function to be executed as soon as possible, but only after all current I/O callbacks have been processed during the current poll phase. This means that process.nextTick tasks are executed before any other scheduled tasks (timers and I/O callbacks) but after any other process.nextTick calls in the same event loop iteration.
  1. What is the difference between the call stack and the task queue? The call stack is responsible for executing synchronous code, while the task queue holds asynchronous tasks that need to be executed when the call stack is empty. Synchronous functions are pushed onto the call stack and executed one by one until they return or throw an error. Asynchronous functions, on the other hand, are added to the task queue and executed when the call stack is empty.
  1. What is event loop concurrency in Node.js? Event loop concurrency refers to the number of asynchronous operations that can be handled simultaneously by the event loop in a single Node.js process. While Node.js can handle multiple I/
Node Event Loop (JavaScript) | JavaScript | XQA Learn