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!');
Output:
Start reading the file...
<contents of example.txt>
File read completed!
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:
-
Timers: Executes callbacks scheduled by
setTimeout
andsetInterval
. - Pending Callbacks: Executes I/O callbacks that were deferred.
- Idle, Prepare: Used internally by Node.js.
- Poll: Retrieves new I/O events; executes I/O callbacks (file reading, network communication).
-
Check: Executes callbacks from
setImmediate
. - 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');
Output:
Start
End
Inside setTimeout
Explanation:
- The code starts by logging "Start".
-
setTimeout
schedules a callback to be executed after 0 milliseconds and places it in the timers queue. - "End" is logged next because the main thread continues executing synchronous code.
- 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...');
Output:
Start reading file...
File reading initiated...
File content: (content of example.txt)
Explanation:
- "Start reading file..." is logged.
-
fs.readFile
initiates a file read operation. Node.js offloads this task to the system kernel, which handles it asynchronously. - The main thread continues executing the next line and logs "File reading initiated...".
- When the file read operation completes, the kernel notifies Node.js, which places the callback into the event queue.
- 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/');
Explanation:
- "Start server..." is logged.
-
http.createServer
sets up a server. The callback (handling requests) is registered with the event loop but does not block the main thread. - "Server running at http://localhost:3000/" is logged.
- 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...');
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...');
Explanation:
- Using Promises makes the asynchronous code more readable.
-
then
handles the successful read, andcatch
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...');
Explanation:
-
async
marks the function as asynchronous, allowing the use ofawait
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)