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.
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
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.
1console.log('First');23setTimeout(() => {4 console.log('Second');5}, 0);67console.log('Third');89// Output:10// First11// Third12// 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
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 are functions passed as arguments to be executed later when an asynchronous operation completes.
1const fs = require('fs');23// Asynchronous file reading with callback4fs.readFile('file.txt', 'utf8', (err, data) => {5 if (err) {6 console.error('Error:', err);7 return;8 }9 console.log('File contents:', data);10});1112console.log('Reading file...');1314// 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":
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 provide a cleaner way to handle asynchronous operations. A promise represents a value that may be available now, in the future, or never.
1const fs = require('fs').promises;23// Using promises4fs.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
1function delay(ms) {2 return new Promise(resolve => {3 setTimeout(() => resolve(), ms);4 });5}67delay(2000)8 .then(() => console.log('Executed after 2 seconds'));
Async/await makes asynchronous code look and behave more like synchronous code, making it easier to read and maintain.
1const fs = require('fs').promises;23async function readFiles() {4 try {5 const data1 = await fs.readFile('file1.txt', 'utf8');6 console.log('File 1:', data1);78 const data2 = await fs.readFile('file2.txt', 'utf8');9 console.log('File 2:', data2);1011 return { data1, data2 };12 } catch (err) {13 console.error('Error reading files:', err);14 throw err;15 }16}1718readFiles();
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:
1async function readFilesParallel() {2 try {3 // Read both files simultaneously4 const [data1, data2] = await Promise.all([5 fs.readFile('file1.txt', 'utf8'),6 fs.readFile('file2.txt', 'utf8')7 ]);89 console.log('File 1:', data1);10 console.log('File 2:', data2);11 } catch (err) {12 console.error('Error:', err);13 }14}
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:
1// math.js2function add(a, b) {3 return a + b;4}56function subtract(a, b) {7 return a - b;8}910module.exports = { add, subtract };
Using the module:
1// app.js2const math = require('./math');34console.log(math.add(5, 3)); // 85console.log(math.subtract(5, 3)); // 267// Or use destructuring8const { 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:
1// math.mjs2export function add(a, b) {3 return a + b;4}56export function subtract(a, b) {7 return a - b;8}910// Default export11export default function multiply(a, b) {12 return a * b;13}
Using ES modules:
1// app.mjs2import multiply, { add, subtract } from './math.mjs';34console.log(add(5, 3)); // 85console.log(subtract(5, 3)); // 26console.log(multiply(5, 3)); // 15
Built-in Modules
Node.js comes with many built-in modules you can use without installation:
const fs = require('fs'); // File systemconst path = require('path'); // Path utilitiesconst http = require('http'); // HTTP serverconst crypto = require('crypto'); // Cryptographyconst os = require('os'); // Operating system infoconst 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!