Node.js is built around an event-driven architecture, and the EventEmitter class is one of the core pieces behind that design. It provides a simple way for one part of an application to publish events while other parts react to them without being tightly coupled.
You encounter this pattern throughout Node.js, from HTTP servers and streams to custom application-level workflows. Understanding EventEmitter makes it easier to reason about asynchronous code, notifications, plugins, background jobs, and internal application events.
In this article, we will build a practical EventEmitter example from scratch. We will see how to create an emitter, register listeners, pass data through events, handle errors, remove listeners, and understand when this pattern is useful in production applications.
Understanding Node.js EventEmitter Through a Practical Example
The EventEmitter class is available from Node.js's built-in events module. An emitter maintains a collection of listeners for named events, and calling emit() synchronously invokes the listeners registered for that event.
A useful mental model is to think of emit() as publishing a message and on() as subscribing to that message. The component producing the event does not need to know which components are listening, which helps separate business logic from side effects such as logging, notifications, analytics, or auditing.
The example below simulates an order-processing workflow. When an order is created, the application emits an orderCreated event, multiple listeners respond to it, an additional listener demonstrates one-time execution with once(), and the example later removes a listener with off().
One important detail is that EventEmitter listeners execute synchronously when emit() is called. EventEmitter itself does not automatically make listener functions asynchronous, so expensive work should be handled carefully and asynchronous operations should be explicitly designed rather than assumed.
Error handling also deserves special attention. An EventEmitter has a special error event, and applications should generally register an error listener when they use emitters that may publish errors; otherwise, an emitted error event can cause the Node.js process to terminate.
const { EventEmitter } = require('events');
// Create a custom EventEmitter for our order workflow.
const orderEvents = new EventEmitter();
console.log('1. Order event system initialized');
// Listener 1: send a confirmation message.
function sendConfirmation(order) {
console.log(`2. Confirmation sent for order ${order.id}`);
console.log(` Customer: ${order.customer}`);
}
// Listener 2: record the order for analytics.
function recordAnalytics(order) {
console.log(`3. Analytics recorded for ${order.id}`);
console.log(` Total value: ₹${order.total}`);
}
// Listener 3: simulate inventory processing.
function updateInventory(order) {
console.log(`4. Inventory updated for ${order.id}`);
console.log(` Items reserved: ${order.items.length}`);
}
// Register multiple listeners for the same event.
orderEvents.on('orderCreated', sendConfirmation);
orderEvents.on('orderCreated', recordAnalytics);
orderEvents.on('orderCreated', updateInventory);
// once() runs the listener only the first time the event is emitted.
orderEvents.once('orderCreated', (order) => {
console.log(`5. First-order welcome notification for ${order.customer}`);
});
// Handle the special EventEmitter error event.
orderEvents.on('error', (error) => {
console.log(`ERROR HANDLER: ${error.message}`);
});
// Listen for an order completion event.
orderEvents.on('orderCompleted', (order) => {
console.log(`6. Order ${order.id} completed successfully`);
});
// Create sample order data.
const order = {
id: 'ORD-1001',
customer: 'Ansh',
total: 1499,
items: ['Keyboard', 'Mouse', 'USB Hub']
};
console.log('\n--- Creating first order ---');
// emit() synchronously calls every registered listener.
orderEvents.emit('orderCreated', order);
console.log('\n--- Completing order ---');
// Emit another custom event with the same object.
orderEvents.emit('orderCompleted', order);
console.log('\n--- Listener information ---');
console.log(
`orderCreated listeners: ${orderEvents.listenerCount('orderCreated')}`
);
// Remove the inventory listener when it is no longer needed.
orderEvents.off('orderCreated', updateInventory);
console.log('\n--- Creating second order ---');
const secondOrder = {
id: 'ORD-1002',
customer: 'Rahul',
total: 899,
items: ['Headphones']
};
// The removed listener will not execute for this order.
orderEvents.emit('orderCreated', secondOrder);
console.log('\n--- Testing error event ---');
// Emitting an error is safe because an error listener is registered.
orderEvents.emit('error', new Error('Payment gateway temporarily unavailable'));
console.log('\n--- Final listener information ---');
console.log(
`orderCreated listeners: ${orderEvents.listenerCount('orderCreated')}`
);
console.log('Event workflow finished successfully.');
Conclusion
Node.js EventEmitter is a lightweight foundation for building event-driven application behavior. Its biggest advantage is decoupling: the component that emits an event does not need to directly manage every action that should happen afterward.
The core methods are straightforward: on() registers a listener, once() registers a listener that runs once, emit() publishes an event, and off() removes a listener. These primitives are enough to build useful internal event systems for orders, authentication, logging, notifications, queues, and application lifecycle events.
EventEmitter should still be used intentionally. Keep event names predictable, avoid hidden business-critical dependencies, clean up listeners when necessary, and always handle the special error event when an emitter can produce errors.
Once you understand EventEmitter, many parts of Node.js become easier to understand because streams, servers, and other core APIs frequently rely on event-driven patterns. It is a small API, but mastering it gives you a much stronger foundation for designing scalable Node.js applications.
Top comments (0)