Node.js Fundamentals

Master the core concepts that make Node.js unique and powerful. Learn about the event loop, asynchronous programming patterns, and the module system.

Introduction

Node.js is built on Chrome's V8 JavaScript engine and uses an event-driven, non-blocking I/O model. This makes it lightweight and efficient, perfect for data-intensive real-time applications.

Key Characteristics

  • Single-threaded: Uses one main thread for JavaScript execution
  • Event-driven: Responds to events asynchronously
  • Non-blocking I/O: Doesn't wait for I/O operations to complete
  • Scalable: Can handle thousands of concurrent connections
Event Loop

The event loop is what allows Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. It offloads operations to the system kernel whenever possible.

js
1console.log('First');
2
3setTimeout(() => {
4 console.log('Second');
5}, 0);
6
7console.log('Third');
8
9// Output:
10// First
11// Third
12// Second

Even though setTimeout has a delay of 0ms, "Second" is printed last because the callback is placed in the event queue and executed after the current call stack is empty.

Event Loop Phases

  • Timers: Executes callbacks scheduled by setTimeout() and setInterval()
  • Pending callbacks: Executes I/O callbacks deferred to the next loop iteration
  • Poll: Retrieves new I/O events
  • Check: Executes setImmediate() callbacks
  • Close callbacks: Executes close event callbacks
Asynchronous Programming

Node.js uses asynchronous programming to handle multiple operations concurrently without blocking the main thread. This is essential for building performant applications.

Why Asynchronous?

Synchronous code blocks execution until an operation completes. In a web server, this would mean handling only one request at a time. Asynchronous code allows the server to handle multiple requests while waiting for I/O operations (database queries, file reads, API calls) to complete.

Callbacks

Callbacks are functions passed as arguments to be executed later when an asynchronous operation completes.

js
1const fs = require('fs');
2
3// Asynchronous file reading with callback
4fs.readFile('file.txt', 'utf8', (err, data) => {
5 if (err) {
6 console.error('Error:', err);
7 return;
8 }
9 console.log('File contents:', data);
10});
11
12console.log('Reading file...');
13
14// Output:
15// Reading file...
16// File contents: [contents of file.txt]

Callback Hell

When multiple callbacks are nested, code becomes difficult to read and maintain. This is known as "callback hell" or "pyramid of doom":

js
fs.readFile('file1.txt', (err, data1) => {
fs.readFile('file2.txt', (err, data2) => {
fs.readFile('file3.txt', (err, data3) => {
// More nesting...
});
});
});

Promises and async/await solve this problem elegantly.

Promises

Promises provide a cleaner way to handle asynchronous operations. A promise represents a value that may be available now, in the future, or never.

js
1const fs = require('fs').promises;
2
3// Using promises
4fs.readFile('file.txt', 'utf8')
5 .then(data => {
6 console.log('File contents:', data);
7 return fs.readFile('file2.txt', 'utf8');
8 })
9 .then(data2 => {
10 console.log('File 2 contents:', data2);
11 })
12 .catch(err => {
13 console.error('Error:', err);
14 });

Promise States

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed

Creating a Promise

js
1function delay(ms) {
2 return new Promise(resolve => {
3 setTimeout(() => resolve(), ms);
4 });
5}
6
7delay(2000)
8 .then(() => console.log('Executed after 2 seconds'));
Async/Await

Async/await makes asynchronous code look and behave more like synchronous code, making it easier to read and maintain.

js
1const fs = require('fs').promises;
2
3async function readFiles() {
4 try {
5 const data1 = await fs.readFile('file1.txt', 'utf8');
6 console.log('File 1:', data1);
7
8 const data2 = await fs.readFile('file2.txt', 'utf8');
9 console.log('File 2:', data2);
10
11 return { data1, data2 };
12 } catch (err) {
13 console.error('Error reading files:', err);
14 throw err;
15 }
16}
17
18readFiles();

Best Practice

Use async/await for cleaner, more readable asynchronous code. It's now the preferred approach in modern Node.js applications.

Parallel Execution

Use Promise.all() to execute multiple promises in parallel:

js
1async function readFilesParallel() {
2 try {
3 // Read both files simultaneously
4 const [data1, data2] = await Promise.all([
5 fs.readFile('file1.txt', 'utf8'),
6 fs.readFile('file2.txt', 'utf8')
7 ]);
8
9 console.log('File 1:', data1);
10 console.log('File 2:', data2);
11 } catch (err) {
12 console.error('Error:', err);
13 }
14}
Module System

Node.js uses modules to organize code into reusable pieces. By default, Node.js uses the CommonJS module system, but also supports ES modules.

CommonJS (require/module.exports)

Creating a module:

js
1// math.js
2function add(a, b) {
3 return a + b;
4}
5
6function subtract(a, b) {
7 return a - b;
8}
9
10module.exports = { add, subtract };

Using the module:

js
1// app.js
2const math = require('./math');
3
4console.log(math.add(5, 3)); // 8
5console.log(math.subtract(5, 3)); // 2
6
7// Or use destructuring
8const { add, subtract } = require('./math');
9console.log(add(5, 3)); // 8

ES Modules (import/export)

To use ES modules, either use .mjs extension or add "type": "module" to package.json:

js
1// math.mjs
2export function add(a, b) {
3 return a + b;
4}
5
6export function subtract(a, b) {
7 return a - b;
8}
9
10// Default export
11export default function multiply(a, b) {
12 return a * b;
13}

Using ES modules:

js
1// app.mjs
2import multiply, { add, subtract } from './math.mjs';
3
4console.log(add(5, 3)); // 8
5console.log(subtract(5, 3)); // 2
6console.log(multiply(5, 3)); // 15

Built-in Modules

Node.js comes with many built-in modules you can use without installation:

js
const fs = require('fs'); // File system
const path = require('path'); // Path utilities
const http = require('http'); // HTTP server
const crypto = require('crypto'); // Cryptography
const os = require('os'); // Operating system info
const events = require('events'); // Event emitter

Next Steps

Now that you understand the fundamentals, you're ready to learn about npm and creating your own modules!

Built for learning — keep experimenting!