Nodejs

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 error event. 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.created or user:login over generic names like created or login. 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 use once(). 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 wrapping on in a manual promise constructor and handles edge cases (like error events) correctly.

References

Powered by Contentful