Node.js: Callbacks, Events, and Packaging
The Callback Concept
Node.js is asynchronous — long-running operations (file I/O, network calls) don't block execution. Instead, you pass a callback function that runs once the operation finishes.
const fs = require('fs');
console.log("1. Start reading file");
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
console.error("Error:", err);
return;
}
console.log("3. File contents:", data);
});
console.log("2. This runs before the file is read!");
Output order: 1 → 2 → 3, because readFile is non-blocking — Node moves on immediately and invokes the callback later when the disk operation completes.
Callback Hell
Nesting many callbacks becomes hard to read:
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details); // deeply nested — "callback hell"
});
});
});
Modern Node.js code prefers Promises and async/await to flatten this structure, though callbacks remain foundational to Node's core APIs.
Events and the EventEmitter
Many Node.js core modules are built around an event-driven architecture. The events module's EventEmitter class lets objects emit named events that other code can listen for.
const EventEmitter = require('events');
class OrderEmitter extends EventEmitter {}
const order = new OrderEmitter();
// Register a listener
order.on('placed', (orderId) => {
console.log(`Order ${orderId} was placed!`);
});
order.on('placed', (orderId) => {
console.log(`Sending confirmation email for order ${orderId}`);
});
// Emit the event
order.emit('placed', 101);
Key EventEmitter Methods
| Method | Purpose |
|---|---|
on(event, listener) | Register a listener (fires every time) |
once(event, listener) | Register a listener that fires only once |
emit(event, ...args) | Trigger the event, calling all listeners |
removeListener(event, listener) | Unregister a specific listener |
removeAllListeners(event) | Remove all listeners for an event |
Packaging: Modules in Node.js
Node.js splits code into modules — each file is its own module with a private scope.
Exporting a Module
// mathUtils.js
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
module.exports = { add, subtract };
Importing a Module
// app.js
const { add, subtract } = require('./mathUtils');
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
Types of Modules
| Type | Example | Loaded via |
|---|---|---|
| Core (built-in) | fs, http, path, events | require('fs') |
| Local | Your own .js files | require('./file') |
| Third-party | Installed via NPM | require('express') |
Publishing a Package
A well-packaged Node.js module needs a proper package.json:
{
"name": "my-math-utils",
"version": "1.0.0",
"main": "index.js",
"description": "Simple math utility functions",
"keywords": ["math", "utils"],
"license": "MIT"
}
npm login
npm publish # publishes to the npm registry
Key Takeaway: Node.js's non-blocking model relies on callbacks to signal completion, andEventEmitterformalizes this into a publish/subscribe pattern used throughout core modules likehttpandfs. Every file is a module — export withmodule.exports, import withrequire(), and describe the package withpackage.json.