Event-Driven Development with EventEmitter
Node.js uses an event-driven architecture. Many core APIs (like HTTP servers, streams, and file streams) inherit from the EventEmitter class. This class provides the base functionality to emit named events and register callback handlers.
1. Basic Usage of EventEmitter
To get started, instantiate the EventEmitter class from the events core module:
import { EventEmitter } from "events";
const myEmitter = new EventEmitter();
// Register a listener for the "greet" event
myEmitter.on("greet", (name) => {
console.log(`Hello, ${name}!`);
});
// Emit the "greet" event with data
myEmitter.emit("greet", "Alice");2. Registering and Removing Listeners
You can control listener behavior by choosing whether they trigger repeatedly, and unregister them when they are no longer needed:
- on(eventName, listener): Triggers the listener function every time the event is emitted.
- once(eventName, listener): Triggers the listener function at most once. The listener is automatically removed after execution.
- off(eventName, listener) / removeListener(eventName, listener): Unregisters a specific listener function.
import { EventEmitter } from "events";
const emitter = new EventEmitter();
const onMessage = (msg) => {
console.log("Message received:", msg);
};
// Add listener
emitter.on("message", onMessage);
// Emit twice
emitter.emit("message", "First transmission");
emitter.emit("message", "Second transmission");
// Remove listener
emitter.off("message", onMessage);
// Emitting now does nothing since all listeners are removed
emitter.emit("message", "Third transmission");3. Creating Custom Event Emitter Classes
In real-world applications, you should extend the EventEmitter class to build custom components that emit their own state transitions.
Here is a practical simulation of a file downloader class:
import { EventEmitter } from "events";
class FileDownloader extends EventEmitter {
constructor(fileName) {
super();
this.fileName = fileName;
}
start() {
this.emit("start", this.fileName);
let progress = 0;
const interval = setInterval(() => {
progress += 25;
this.emit("progress", progress);
if (progress >= 100) {
clearInterval(interval);
this.emit("complete", this.fileName);
}
}, 500);
}
}
// Instantiate and use the custom emitter
const downloader = new FileDownloader("invoice.pdf");
downloader.on("start", (file) => {
console.log(`Starting download for: ${file}`);
});
downloader.on("progress", (percent) => {
console.log(`Progress: ${percent}%`);
});
downloader.on("complete", (file) => {
console.log(`Download finished: ${file}`);
});
downloader.start();4. Error Event Handling
If an EventEmitter instance emits an "error" event and there are no listeners registered for it, the Node.js process will print a stack trace and crash.
Always register an error listener on event-heavy objects:
import { EventEmitter } from "events";
const emitter = new EventEmitter();
// Failing to register this listener will cause the process to crash
emitter.on("error", (err) => {
console.error("Successfully caught custom error event:", err.message);
});
// Emit error
emitter.emit("error", new Error("Database connection timeout"));