DEV Community

Abdelhakim mohamed
Abdelhakim mohamed

Posted on

Mastering Node.js: A Comprehensive Tutorial Series Part 4 - Event Loop

Introduction to the Node.js Event Loop

The event loop lets Node.js handle non-blocking I/O operations, even though JavaScript is single-threaded, by offloading tasks to the system kernel. This makes Node.js efficient and scalable. In this article, we’ll explore how the event loop works and why it’s essential for handling tasks like file I/O and network requests.

Reference

Video for better understanding

How the Event Loop Works

Node.js runs on a single thread, meaning it executes one command at a time. However, it offloads heavy or long-running operations (like I/O tasks) to the system kernel. The kernel can handle multiple operations in the background. When these operations are completed, they are placed in the event queue, and the event loop picks them up for further processing.

What Are Synchronous Operations in Node.js?

Synchronous operations in Node.js are tasks that are executed sequentially, one at a time. Each operation waits for the previous one to complete before moving forward. While this method is simple, it can block the execution of other tasks, leading to performance issues if the operations take too long.

In Node.js, synchronous code runs on the main thread, making it inefficient for tasks like file I/O, network requests, or database queries.

Example of Synchronous Code in Node.js

Here’s an example of a synchronous file read operation:

const fs = require('fs');

console.log('Start reading the file...');

const data = fs.readFileSync('example.txt', 'utf8');

console.log(data);
console.log('File read completed!');
Enter fullscreen mode Exit fullscreen mode

Output:

Start reading the file...
<contents of example.txt>
File read completed!
Enter fullscreen mode Exit fullscreen mode

In this example, fs.readFileSync blocks the execution until the file is completely read, which can delay the rest of the code if the file is large. This is the main drawback of synchronous operations in Node.js.

The Event Loop's Phases

The event loop's primary phases include:

  1. Timers: Executes callbacks scheduled by setTimeout and setInterval.
  2. Pending Callbacks: Executes I/O callbacks that were deferred.
  3. Idle, Prepare: Used internally by Node.js.
  4. Poll: Retrieves new I/O events; executes I/O callbacks (file reading, network communication).
  5. Check: Executes callbacks from setImmediate.
  6. Close Callbacks: Executes callbacks related to closed resources (e.g., socket connections).

Example 1: Simple Event Loop

Here’s a basic example demonstrating how the event loop works:

console.log('Start');

setTimeout(() => {
  console.log('Inside setTimeout');
}, 0);

console.log('End');
Enter fullscreen mode Exit fullscreen mode

Output:

Start
End
Inside setTimeout
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. The code starts by logging "Start".
  2. setTimeout schedules a callback to be executed after 0 milliseconds and places it in the timers queue.
  3. "End" is logged next because the main thread continues executing synchronous code.
  4. The event loop then picks up the setTimeout callback and executes it, logging "Inside setTimeout".

Even though the timeout is set to 0, the callback is placed in the event loop's timer queue and is executed after the main code finishes running. This demonstrates the asynchronous nature of the event loop.

Example 2: File I/O with the Event Loop

Node.js uses the event loop to manage file I/O tasks without blocking the main thread:

const fs = require('fs');

console.log('Start reading file...');

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

console.log('File reading initiated...');
Enter fullscreen mode Exit fullscreen mode

Output:

Start reading file...
File reading initiated...
File content: (content of example.txt)
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. "Start reading file..." is logged.
  2. fs.readFile initiates a file read operation. Node.js offloads this task to the system kernel, which handles it asynchronously.
  3. The main thread continues executing the next line and logs "File reading initiated...".
  4. When the file read operation completes, the kernel notifies Node.js, which places the callback into the event queue.
  5. The event loop picks up the callback and logs the file content.

By using the event loop, Node.js avoids blocking the main thread during the file I/O operation, making it possible to handle multiple tasks concurrently.

Example 3: Network Requests with the Event Loop

Let's see how Node.js handles network requests asynchronously using the event loop:

const http = require('http');

console.log('Start server...');

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

console.log('Server running at http://localhost:3000/');
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. "Start server..." is logged.
  2. http.createServer sets up a server. The callback (handling requests) is registered with the event loop but does not block the main thread.
  3. "Server running at http://localhost:3000/" is logged.
  4. When a request is made to the server, the callback registered with createServer is executed, responding with "Hello World".

The event loop allows Node.js to handle each incoming request asynchronously, making it efficient and scalable for network operations.

Example 4: Asynchronous Code with Callbacks, Promises, and async/await

Using Callbacks

Callbacks are one of the basic ways to handle asynchronous operations in Node.js:

const fs = require('fs');

console.log('Start reading file...');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
    return;
  }
  console.log('File content:', data);
});

console.log('File reading initiated...');
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • fs.readFile is an asynchronous function. It offloads the file reading to the system kernel and immediately moves on to the next line of code.
  • The callback is executed when the file reading is complete.

Using Promises

Promises offer a cleaner way to handle asynchronous operations and avoid callback hell:

const fs = require('fs').promises;

console.log('Start reading file...');

fs.readFile('example.txt', 'utf8')
  .then((data) => {
    console.log('File content:', data);
  })
  .catch((err) => {
    console.error('Error reading file:', err);
  });

console.log('File reading initiated...');
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Using Promises makes the asynchronous code more readable.
  • then handles the successful read, and catch handles any errors.

Using async/await

async/await provides an even more straightforward way to handle asynchronous operations:

const fs = require('fs').promises;

async function readFileAsync() {
  try {
    console.log('Start reading file...');
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('File content:', data);
  } catch (err) {
    console.error('Error reading file:', err);
  }
  console.log('File reading completed.');
}

readFileAsync();

console.log('File reading initiated...');
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • async marks the function as asynchronous, allowing the use of await to pause execution until the Promise resolves.
  • This approach makes the code look synchronous while retaining its non-blocking nature.

Why the Event Loop is Essential

The event loop allows Node.js to manage multiple tasks (file I/O, network requests, database queries, etc.) concurrently without blocking the main thread. It offloads heavy operations to the system kernel or background workers and processes their callbacks when ready. This makes Node.js highly efficient for building scalable applications, such as servers handling numerous simultaneous connections.


Top comments (0)