Event Emitters: Patterns and Best Practices
A comprehensive guide to Node.js EventEmitter covering event-driven architecture, listener management, error handling, async patterns, and building decoupled systems.
Event Emitters: Patterns and Best Practices
Overview
The EventEmitter class is the backbone of Node.js event-driven architecture. It provides a publish-subscribe mechanism that lets you decouple components, react to state changes, and build systems where modules communicate without knowing about each other. If you have worked with streams, HTTP servers, or even process itself, you have already used EventEmitter -- understanding it deeply unlocks cleaner, more maintainable Node.js applications.
Prerequisites
- Node.js v14 or later installed
- Solid understanding of JavaScript fundamentals (prototypes, callbacks)
- Familiarity with Node.js module system (
require) - Basic understanding of asynchronous programming in Node.js
EventEmitter Basics
The events module ships with Node.js. No installation required.
var EventEmitter = require("events");
var emitter = new EventEmitter();
// Register a listener
emitter.on("user:login", function (user) {
console.log("User logged in:", user.name);
});
// Emit an event
emitter.emit("user:login", { name: "Shane", role: "admin" });
User logged in: Shane
Core Methods
The API is small and deliberate. Here are the methods you will use daily:
var EventEmitter = require("events");
var emitter = new EventEmitter();
// on() - Register a persistent listener
emitter.on("data", function (payload) {
console.log("Received:", payload);
});
// once() - Register a one-time listener (auto-removes after first call)
emitter.once("init", function () {
console.log("Initialized (this only fires once)");
});
// emit() - Fire an event with optional arguments
emitter.emit("data", { id: 1 });
emitter.emit("init");
emitter.emit("init"); // Nothing happens -- listener already removed
// removeListener() / off() - Remove a specific listener
var handler = function () {
console.log("I will be removed");
};
emitter.on("temp", handler);
emitter.removeListener("temp", handler); // or emitter.off("temp", handler)
emitter.emit("temp"); // Nothing happens
// removeAllListeners() - Nuclear option
emitter.removeAllListeners("data");
// listenerCount() - Check how many listeners exist
console.log(emitter.listenerCount("data")); // 0
One important detail: emit() returns true if the event had listeners, false otherwise. This is useful for checking whether anyone is listening before doing expensive work.
if (emitter.emit("expensive:operation", data)) {
console.log("Someone handled it");
} else {
console.log("No listeners registered, skipping");
}
Extending EventEmitter in Your Classes
This is where EventEmitter becomes powerful. Instead of creating standalone emitters, extend your domain classes with event capabilities.
var EventEmitter = require("events");
var util = require("util");
function UserService(db) {
EventEmitter.call(this);
this.db = db;
}
util.inherits(UserService, EventEmitter);
UserService.prototype.createUser = function (userData) {
var self = this;
// Simulate database insert
var user = { id: Date.now(), name: userData.name, email: userData.email };
self.emit("user:creating", userData);
// Simulate async DB operation
setTimeout(function () {
self.emit("user:created", user);
}, 100);
return user;
};
// Usage
var service = new UserService(null);
service.on("user:creating", function (data) {
console.log("Validating user data:", data.name);
});
service.on("user:created", function (user) {
console.log("User created with ID:", user.id);
console.log("Sending welcome email to:", user.email);
});
service.createUser({ name: "Shane", email: "[email protected]" });
Validating user data: Shane
User created with ID: 1707849321456
Sending welcome email to: [email protected]
You can also use ES6 class syntax with extends, which is more common in modern codebases but still uses require:
var EventEmitter = require("events");
class ConnectionPool extends EventEmitter {
constructor(options) {
super();
this.maxConnections = options.maxConnections || 10;
this.connections = [];
}
acquire() {
if (this.connections.length >= this.maxConnections) {
this.emit("pool:exhausted", { max: this.maxConnections });
return null;
}
var conn = { id: Date.now(), active: true };
this.connections.push(conn);
this.emit("connection:acquired", conn);
return conn;
}
release(conn) {
this.connections = this.connections.filter(function (c) {
return c.id !== conn.id;
});
this.emit("connection:released", conn);
}
}
The Error Event Convention
This is the single most important thing to understand about EventEmitter: if you emit an error event and no listener is registered for it, Node.js will throw an uncaught exception and crash your process.
var EventEmitter = require("events");
var emitter = new EventEmitter();
// This CRASHES your process
emitter.emit("error", new Error("Something broke"));
events.js:292
throw er; // Unhandled 'error' event
^
Error: Something broke
at Object.<anonymous> (/app/server.js:4:29)
This is by design. Node.js treats unhandled error events as programmer errors that should not be silently swallowed. The fix is straightforward: always register an error listener.
var EventEmitter = require("events");
var emitter = new EventEmitter();
emitter.on("error", function (err) {
console.error("Caught error:", err.message);
// Log it, report to monitoring, take corrective action
});
emitter.emit("error", new Error("Something broke"));
// Output: Caught error: Something broke
// Process continues running
When building classes that extend EventEmitter, always document which error conditions trigger the error event and always register a handler:
var EventEmitter = require("events");
var util = require("util");
function FileProcessor() {
EventEmitter.call(this);
}
util.inherits(FileProcessor, EventEmitter);
FileProcessor.prototype.process = function (filePath) {
var self = this;
var fs = require("fs");
fs.readFile(filePath, "utf8", function (err, data) {
if (err) {
// Emit error event -- callers MUST register an error handler
self.emit("error", err);
return;
}
self.emit("file:processed", { path: filePath, size: data.length });
});
};
Listener Ordering and Execution
Listeners execute synchronously in the order they were registered. This is not obvious and has real implications.
var EventEmitter = require("events");
var emitter = new EventEmitter();
emitter.on("request", function () {
console.log("1. Authentication check");
});
emitter.on("request", function () {
console.log("2. Rate limiting");
});
emitter.on("request", function () {
console.log("3. Request logging");
});
emitter.emit("request");
1. Authentication check
2. Rate limiting
3. Request logging
If you need a listener to run first, use prependListener():
emitter.prependListener("request", function () {
console.log("0. This runs before everything else");
});
The synchronous execution model means a slow listener blocks all subsequent listeners. If any listener throws, the remaining listeners do not execute. Keep this in mind when designing your event flows.
Max Listeners Warning and When to Increase It
By default, EventEmitter warns if you add more than 10 listeners for a single event. This is a memory leak detector, not a hard limit.
var EventEmitter = require("events");
var emitter = new EventEmitter();
for (var i = 0; i < 11; i++) {
emitter.on("data", function () {});
}
(node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
If you legitimately need more than 10 listeners (a message bus, for example), increase the limit:
// Per-emitter
emitter.setMaxListeners(50);
// Globally (affects all new emitters)
EventEmitter.defaultMaxListeners = 50;
// Unlimited (use carefully)
emitter.setMaxListeners(0);
Before increasing the limit, ask yourself whether you actually need that many listeners or whether you have a registration bug. In most applications, hitting this warning indicates a real problem.
Memory Leaks from Forgotten Listeners
This is the most common EventEmitter bug in production. Listeners that are registered but never removed accumulate over time and prevent garbage collection.
var EventEmitter = require("events");
var emitter = new EventEmitter();
emitter.setMaxListeners(0); // Silencing the warning makes it worse
function handleRequest(req, res) {
// BUG: New listener added on every request, never removed
emitter.on("config:update", function () {
res.locals.config = getLatestConfig();
});
res.send("OK");
}
Every incoming request adds a new listener. After 10,000 requests, you have 10,000 listeners sitting in memory. The fix is to use once() or explicitly remove listeners when they are no longer needed:
function handleRequest(req, res) {
var handler = function () {
res.locals.config = getLatestConfig();
};
emitter.on("config:update", handler);
// Clean up when response finishes
res.on("finish", function () {
emitter.removeListener("config:update", handler);
});
res.send("OK");
}
Monitor your listener counts in production:
// Health check endpoint
app.get("/health", function (req, res) {
res.json({
status: "ok",
listeners: {
configUpdate: emitter.listenerCount("config:update"),
dataSync: emitter.listenerCount("data:sync")
}
});
});
Async Event Handling Patterns
EventEmitter listeners execute synchronously. If you need async handling, you have several options.
Pattern 1: Fire and Forget
The simplest approach. Emit the event and let listeners handle async work independently.
emitter.on("order:placed", function (order) {
// Async work inside a sync listener
sendConfirmationEmail(order).catch(function (err) {
console.error("Failed to send email:", err);
});
});
Pattern 2: events.once() Promise Wrapper
Node.js v11.13+ provides a promise-based wrapper for waiting on a single event.
var events = require("events");
var EventEmitter = require("events");
var emitter = new EventEmitter();
async function waitForReady() {
// Returns a promise that resolves with the event arguments
var args = await events.once(emitter, "ready");
console.log("System ready with config:", args[0]);
}
waitForReady();
// Later...
setTimeout(function () {
emitter.emit("ready", { port: 3000 });
}, 1000);
This is particularly useful for initialization sequences:
var events = require("events");
async function startServer(app, db) {
await events.once(db, "connected");
console.log("Database connected");
await events.once(app, "listening");
console.log("Server listening");
console.log("All systems go");
}
Pattern 3: Async Iterator with on()
For consuming a stream of events as an async iterator:
var events = require("events");
var EventEmitter = require("events");
var emitter = new EventEmitter();
async function processEvents() {
var iterator = events.on(emitter, "message");
for await (var args of iterator) {
var message = args[0];
console.log("Processing:", message);
// Async work here
await saveToDatabase(message);
}
}
processEvents();
Wildcard and Namespaced Events with EventEmitter2
The built-in EventEmitter does not support wildcards. If you need pattern-based event matching, use the eventemitter2 package.
npm install eventemitter2
var EventEmitter2 = require("eventemitter2");
var emitter = new EventEmitter2({
wildcard: true,
delimiter: ".",
maxListeners: 20
});
// Listen to all order events
emitter.on("order.*", function (data) {
console.log("Order event:", this.event, data);
});
// Listen to all events in any namespace
emitter.on("**", function (data) {
console.log("Any event:", this.event);
});
emitter.emit("order.created", { id: 1 });
emitter.emit("order.shipped", { id: 1 });
emitter.emit("user.login", { name: "Shane" });
Order event: order.created { id: 1 }
Any event: order.created
Order event: order.shipped { id: 1 }
Any event: order.shipped
Any event: user.login
This is useful for logging, metrics collection, and building middleware-style event pipelines where you want to observe categories of events without registering individual listeners.
Building a Pub/Sub System with EventEmitter
EventEmitter is a natural fit for in-process pub/sub. Here is a simple but production-ready message bus:
var EventEmitter = require("events");
var util = require("util");
function MessageBus() {
EventEmitter.call(this);
this.setMaxListeners(100);
this.history = [];
this.maxHistory = 1000;
}
util.inherits(MessageBus, EventEmitter);
MessageBus.prototype.publish = function (channel, message) {
var envelope = {
channel: channel,
message: message,
timestamp: Date.now(),
id: Math.random().toString(36).substring(2, 15)
};
this.history.push(envelope);
if (this.history.length > this.maxHistory) {
this.history.shift();
}
this.emit(channel, envelope);
this.emit("*", envelope); // Global listener support
return envelope.id;
};
MessageBus.prototype.subscribe = function (channel, handler) {
this.on(channel, handler);
var self = this;
// Return unsubscribe function
return function () {
self.removeListener(channel, handler);
};
};
MessageBus.prototype.getHistory = function (channel, limit) {
var filtered = this.history;
if (channel) {
filtered = filtered.filter(function (e) {
return e.channel === channel;
});
}
return filtered.slice(-(limit || 50));
};
// Usage
var bus = new MessageBus();
var unsubscribe = bus.subscribe("notifications", function (envelope) {
console.log("[" + envelope.channel + "]", envelope.message);
});
bus.publish("notifications", { text: "New order received", priority: "high" });
bus.publish("notifications", { text: "Payment processed", priority: "low" });
// Clean up
unsubscribe();
bus.publish("notifications", { text: "This is not received" });
Domain Events in Application Architecture
Domain events formalize the pub/sub concept within your application architecture. Instead of services calling each other directly, they emit domain events that other modules react to.
var EventEmitter = require("events");
var util = require("util");
// Central domain event dispatcher
function DomainEvents() {
EventEmitter.call(this);
this.setMaxListeners(50);
}
util.inherits(DomainEvents, EventEmitter);
DomainEvents.prototype.raise = function (eventName, data) {
console.log("[DomainEvent] " + eventName);
this.emit(eventName, {
name: eventName,
data: data,
occurredAt: new Date().toISOString()
});
};
var domainEvents = new DomainEvents();
module.exports = domainEvents;
// In userService.js
var domainEvents = require("./domainEvents");
function registerUser(userData) {
var user = saveUser(userData);
domainEvents.raise("UserRegistered", {
userId: user.id,
email: user.email,
plan: user.plan
});
return user;
}
// In emailService.js -- knows nothing about userService
var domainEvents = require("./domainEvents");
domainEvents.on("UserRegistered", function (event) {
sendWelcomeEmail(event.data.email);
});
// In analyticsService.js -- also knows nothing about userService
var domainEvents = require("./domainEvents");
domainEvents.on("UserRegistered", function (event) {
trackSignup(event.data.userId, event.data.plan);
});
This architecture means you can add new reactions to domain events without modifying the service that raises them. Adding audit logging? Just add another listener. Need to notify a third-party API? Another listener. The originating service never changes.
Typed Events for Better Developer Experience
In larger codebases, documenting your event contracts prevents bugs. Even without TypeScript, you can create a registry of known events.
var EVENT_CATALOG = {
"order:created": {
description: "Fired when a new order is placed",
payload: "{ orderId: string, items: Array, total: number, customerId: string }"
},
"order:paid": {
description: "Fired when payment is confirmed",
payload: "{ orderId: string, paymentId: string, amount: number }"
},
"order:shipped": {
description: "Fired when order is handed to carrier",
payload: "{ orderId: string, trackingNumber: string, carrier: string }"
},
"order:error": {
description: "Fired when order processing fails",
payload: "{ orderId: string, error: Error, stage: string }"
}
};
// Validation wrapper
function createTypedEmitter(catalog) {
var EventEmitter = require("events");
var emitter = new EventEmitter();
var originalEmit = emitter.emit.bind(emitter);
emitter.emit = function (eventName) {
if (eventName !== "error" && !catalog[eventName]) {
console.warn("Unknown event emitted: " + eventName);
}
return originalEmit.apply(null, arguments);
};
emitter.catalog = catalog;
return emitter;
}
var orderEvents = createTypedEmitter(EVENT_CATALOG);
orderEvents.emit("order:created", { orderId: "123", items: [], total: 99.99, customerId: "c1" });
orderEvents.emit("typo:event", {}); // Warns: Unknown event emitted: typo:event
Performance Characteristics
EventEmitter is fast. It is essentially an object with arrays of functions. Emitting an event iterates through the listener array and calls each function. There is no serialization, no network overhead, no message queue.
Benchmarks on a modern machine show:
- Emitting to 1 listener: ~50 million ops/sec
- Emitting to 10 listeners: ~8 million ops/sec
- Emitting to 100 listeners: ~800K ops/sec
When should you consider alternatives?
- Cross-process communication: Use Redis pub/sub, RabbitMQ, or NATS
- Persistent events: Use a message queue (events are fire-and-forget by default)
- High fan-out (1000+ listeners): Consider a dedicated event bus library
- Guaranteed delivery: EventEmitter has no retry mechanism; use a proper message broker
- Distributed systems: EventEmitter is in-process only
Complete Working Example: Order Processing System
Here is a realistic order processing system where OrderService emits events and separate modules handle inventory, notifications, analytics, and audit logging -- all fully decoupled.
orderEvents.js
var EventEmitter = require("events");
var util = require("util");
function OrderEventBus() {
EventEmitter.call(this);
this.setMaxListeners(25);
}
util.inherits(OrderEventBus, EventEmitter);
var bus = new OrderEventBus();
module.exports = bus;
orderService.js
var orderEvents = require("./orderEvents");
var orders = {};
function createOrder(customerId, items) {
var order = {
id: "ORD-" + Date.now(),
customerId: customerId,
items: items,
total: items.reduce(function (sum, item) { return sum + item.price * item.qty; }, 0),
status: "created",
createdAt: new Date().toISOString()
};
orders[order.id] = order;
orderEvents.emit("order.created", order);
return order;
}
function validateOrder(orderId) {
var order = orders[orderId];
if (!order) {
orderEvents.emit("error", new Error("Order not found: " + orderId));
return;
}
// Simulate validation logic
if (order.total <= 0) {
order.status = "invalid";
orderEvents.emit("order.error", { orderId: orderId, reason: "Invalid total" });
return;
}
order.status = "validated";
orderEvents.emit("order.validated", order);
}
function processPayment(orderId) {
var order = orders[orderId];
if (!order || order.status !== "validated") {
orderEvents.emit("error", new Error("Cannot pay for order in status: " + (order ? order.status : "not found")));
return;
}
// Simulate payment processing
setTimeout(function () {
order.status = "paid";
order.paymentId = "PAY-" + Date.now();
orderEvents.emit("order.paid", order);
}, 200);
}
function shipOrder(orderId) {
var order = orders[orderId];
if (!order || order.status !== "paid") {
orderEvents.emit("error", new Error("Cannot ship unpaid order: " + orderId));
return;
}
order.status = "shipped";
order.trackingNumber = "TRK" + Math.random().toString(36).substring(2, 10).toUpperCase();
order.shippedAt = new Date().toISOString();
orderEvents.emit("order.shipped", order);
}
module.exports = {
createOrder: createOrder,
validateOrder: validateOrder,
processPayment: processPayment,
shipOrder: shipOrder
};
inventoryListener.js
var orderEvents = require("./orderEvents");
orderEvents.on("order.created", function (order) {
console.log("[Inventory] Reserving stock for order " + order.id);
order.items.forEach(function (item) {
console.log(" - Reserved " + item.qty + "x " + item.name);
});
});
orderEvents.on("order.shipped", function (order) {
console.log("[Inventory] Deducting stock for shipped order " + order.id);
});
orderEvents.on("order.error", function (data) {
console.log("[Inventory] Releasing reserved stock for order " + data.orderId);
});
module.exports = {}; // Listeners register on require()
notificationListener.js
var orderEvents = require("./orderEvents");
orderEvents.on("order.created", function (order) {
console.log("[Notify] Sending order confirmation to customer " + order.customerId);
});
orderEvents.on("order.paid", function (order) {
console.log("[Notify] Payment receipt sent for order " + order.id + " (Payment: " + order.paymentId + ")");
});
orderEvents.on("order.shipped", function (order) {
console.log("[Notify] Shipping notification sent -- tracking: " + order.trackingNumber);
});
module.exports = {};
analyticsListener.js
var orderEvents = require("./orderEvents");
var metrics = {
ordersCreated: 0,
ordersShipped: 0,
totalRevenue: 0,
errors: 0
};
orderEvents.on("order.created", function (order) {
metrics.ordersCreated++;
console.log("[Analytics] Order created. Total orders: " + metrics.ordersCreated);
});
orderEvents.on("order.paid", function (order) {
metrics.totalRevenue += order.total;
console.log("[Analytics] Revenue updated: $" + metrics.totalRevenue.toFixed(2));
});
orderEvents.on("order.shipped", function (order) {
metrics.ordersShipped++;
});
orderEvents.on("order.error", function () {
metrics.errors++;
});
module.exports = { getMetrics: function () { return metrics; } };
auditListener.js
var orderEvents = require("./orderEvents");
var auditLog = [];
function logEvent(eventName) {
return function (data) {
var entry = {
event: eventName,
orderId: data.id || data.orderId,
timestamp: new Date().toISOString(),
snapshot: JSON.parse(JSON.stringify(data))
};
auditLog.push(entry);
console.log("[Audit] " + eventName + " -- Order " + entry.orderId);
};
}
orderEvents.on("order.created", logEvent("order.created"));
orderEvents.on("order.validated", logEvent("order.validated"));
orderEvents.on("order.paid", logEvent("order.paid"));
orderEvents.on("order.shipped", logEvent("order.shipped"));
module.exports = { getAuditLog: function () { return auditLog; } };
main.js -- Orchestration
var orderService = require("./orderService");
var orderEvents = require("./orderEvents");
// Register listener modules (they self-register on require)
require("./inventoryListener");
require("./notificationListener");
require("./analyticsListener");
var audit = require("./auditListener");
// Global error handler
orderEvents.on("error", function (err) {
console.error("[Error Handler]", err.message);
});
// Process an order through the full lifecycle
var order = orderService.createOrder("CUST-42", [
{ name: "Node.js in Practice", price: 39.99, qty: 1 },
{ name: "EventEmitter Stickers", price: 4.99, qty: 3 }
]);
orderService.validateOrder(order.id);
orderService.processPayment(order.id);
// Payment is async, so ship after it completes
orderEvents.once("order.paid", function (paidOrder) {
orderService.shipOrder(paidOrder.id);
// Print audit log
console.log("\n--- Audit Log ---");
audit.getAuditLog().forEach(function (entry) {
console.log(entry.timestamp + " | " + entry.event + " | " + entry.orderId);
});
});
Output
[Inventory] Reserving stock for order ORD-1707849321456
- Reserved 1x Node.js in Practice
- Reserved 3x EventEmitter Stickers
[Notify] Sending order confirmation to customer CUST-42
[Analytics] Order created. Total orders: 1
[Audit] order.created -- Order ORD-1707849321456
[Audit] order.validated -- Order ORD-1707849321456
[Notify] Payment receipt sent for order ORD-1707849321456 (Payment: PAY-1707849321658)
[Analytics] Revenue updated: $54.96
[Audit] order.paid -- Order ORD-1707849321456
[Inventory] Deducting stock for shipped order ORD-1707849321456
[Notify] Shipping notification sent -- tracking: TRKA7F3BX2K
[Audit] order.shipped -- Order ORD-1707849321456
--- Audit Log ---
2026-02-13T10:15:21.456Z | order.created | ORD-1707849321456
2026-02-13T10:15:21.457Z | order.validated | ORD-1707849321456
2026-02-13T10:15:21.658Z | order.paid | ORD-1707849321456
2026-02-13T10:15:21.659Z | order.shipped | ORD-1707849321456
Notice how orderService.js has zero knowledge of inventory, notifications, analytics, or audit logging. You can add or remove listener modules without touching the service. This is the real power of event-driven architecture.
Common Issues and Troubleshooting
1. Unhandled Error Event Crash
Error:
events.js:292
throw er; // Unhandled 'error' event
^
Error: ECONNREFUSED 127.0.0.1:5432
Cause: An error event was emitted with no listener registered. This is the most common EventEmitter crash.
Fix: Always register an error listener on any emitter that might emit errors.
emitter.on("error", function (err) {
logger.error("EventEmitter error:", err);
});
2. MaxListenersExceededWarning Memory Leak
Warning:
(node:8432) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 connection listeners added to [Server]. Use emitter.setMaxListeners() to increase limit
Cause: Listeners are being added repeatedly (often inside request handlers or loops) without being removed.
Fix: Do not register listeners inside frequently-called functions. If you must, use once() or explicitly remove listeners when done. Check with emitter.listenerCount("eventName") to verify counts are stable.
3. Listener Called After Removal Expected
Symptom: A listener still fires even though you called removeListener().
Cause: You passed a different function reference. Anonymous functions cannot be removed because each declaration creates a new reference.
// BUG: These are different function references
emitter.on("data", function () { console.log("A"); });
emitter.removeListener("data", function () { console.log("A"); }); // Does nothing!
// FIX: Store the reference
var handler = function () { console.log("A"); };
emitter.on("data", handler);
emitter.removeListener("data", handler); // Works
4. Event Listener Throws and Kills Subsequent Listeners
Error:
TypeError: Cannot read properties of undefined (reading 'id')
Cause: One listener threw an error, preventing all subsequent listeners from executing. Since listeners run synchronously, an uncaught throw in listener #2 means listeners #3, #4, etc. never run.
Fix: Wrap listener bodies in try/catch if they might fail:
emitter.on("data", function (payload) {
try {
riskyOperation(payload);
} catch (err) {
console.error("Listener failed:", err.message);
// Other listeners still execute
}
});
5. events.once() Never Resolves
Symptom: await events.once(emitter, "ready") hangs forever.
Cause: The event was emitted before events.once() was called, or the event name has a typo.
Fix: Ensure the listener is registered before the emit can happen. Use AbortController to add a timeout:
var events = require("events");
var ac = new AbortController();
setTimeout(function () { ac.abort(); }, 5000);
try {
await events.once(emitter, "ready", { signal: ac.signal });
} catch (err) {
if (err.code === "ABORT_ERR") {
console.error("Timed out waiting for ready event");
}
}
Best Practices
Always handle the
errorevent. On every EventEmitter instance you create or receive, register an error listener. Unhandled error events crash your process. No exceptions.Use namespaced event names. Prefer
order.createdoruser:loginover generic names likecreatedorlogin. Namespacing prevents collisions and makes debugging easier when you see event names in logs.Prefer
once()for one-time events. Initialization events, connection events, and setup events should useonce(). It self-cleans and prevents accidental double-handling.Keep listeners lightweight. Listeners should delegate to service functions, not contain business logic themselves. A listener that calls
inventoryService.reserve(order)is easier to test and maintain than one with inline database queries.Document your event contracts. Maintain a catalog of event names, their payload shapes, and when they fire. Future developers (including future you) will thank you.
Clean up listeners in teardown. In tests, HTTP request handlers, and WebSocket connections, always remove listeners when the context ends. Use
removeListener(),removeAllListeners(), or the unsubscribe pattern.Monitor listener counts in production. Expose
listenerCount()in health checks. A steadily increasing count is a memory leak.Do not silence the MaxListeners warning without investigation. Calling
setMaxListeners(0)hides real bugs. Increase the limit only when you have a legitimate reason and document why.Wrap risky listeners in try/catch. A throwing listener kills the entire emit chain. Defensive listeners keep your system resilient.
Use
events.once()for promise-based flows. It is cleaner than wrappingonin a manual promise constructor and handles edge cases (like error events) correctly.