Skip to content

Node.js EventEmitter

EventEmitter is the built-in class that powers Node's events pattern. This lesson shows how to use it directly and how to build your own custom event-driven classes on top of it.

The EventEmitter Class

EventEmitter, from the node:events module, is the class every built-in emitter (HTTP servers, streams, etc.) is either an instance of or extends. You can use it directly, or extend it in your own classes to give them the same .on()/.emit() API.

Internally, an EventEmitter just keeps a map of event names to arrays of listener functions, .on() pushes onto that array, and .emit() calls every function in it, passing along whatever arguments you gave to .emit().

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

emitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

emitter.emit('greet', 'Node.js'); // logs: Hello, Node.js!

You can also use EventEmitter directly, without extending it, whenever you just need a standalone pub/sub-style object.

Custom EventEmitter Classes

class OrderProcessor extends EventEmitter {
  process(order) {
    // ... do work ...
    this.emit('processed', order);
  }
}
  • Extend EventEmitter to give your own class .on()/.emit()/.once() methods automatically.
  • Call this.emit('eventName', ...) from inside your class's methods when something noteworthy happens.
  • Consumers of your class can then .on('eventName', handler) without knowing your implementation details.
  • This decouples the class doing the work from the code reacting to its results.

EventEmitter Cheatsheet

The methods you'll use most on any EventEmitter instance.

Method Purpose
.on(event, fn) Add a listener that runs every time
.once(event, fn) Add a listener that runs only the first time
.off(event, fn) Remove a specific listener
.emit(event, ...args) Fire an event, calling all its listeners
.listenerCount(event) Count listeners registered for an event
.setMaxListeners(n) Raise the default 10-listener warning threshold

Building a Custom Event-Driven Class

Extending EventEmitter is a common way to build objects that represent ongoing work, an order processor, a background job runner, a WebSocket wrapper, letting other parts of your code react to lifecycle moments without polling or tight coupling.

import { EventEmitter } from 'node:events';

class UploadTask extends EventEmitter {
  async run(file) {
    this.emit('start', file.name);

    try {
      await saveToDisk(file);
      this.emit('success', file.name);
    } catch (error) {
      this.emit('error', error);
    }
  }
}

const task = new UploadTask();
task.on('start', (name) => console.log('Uploading', name));
task.on('success', (name) => console.log('Done:', name));
task.on('error', (err) => console.error('Upload failed:', err.message));

task.run({ name: 'photo.png' });

The MaxListeners Warning

By default, EventEmitter warns in the console if more than 10 listeners are attached to a single event on the same emitter, this is a safeguard against accidental memory leaks (like re-registering a listener inside a loop). You can raise this limit deliberately with setMaxListeners() when it's genuinely expected.

  • The default limit is a warning, not a hard error, code keeps running past it.
  • Seeing this warning unexpectedly usually means a listener is being added repeatedly instead of once.
  • Call emitter.setMaxListeners(20) (or 0 for unlimited) only when you're sure the higher count is intentional.

Common Mistakes

  • Registering a new listener inside a loop or a frequently-called function, causing listener count to grow unbounded.
  • Extending EventEmitter but forgetting to call events consistently, making the class's behavior hard to predict.
  • Ignoring the MaxListeners warning instead of investigating why so many listeners accumulated.
  • Using .emit() expecting it to behave asynchronously, listeners run synchronously in the same tick.

Key Takeaways

  • EventEmitter (from node:events) is the class underlying Node's entire events pattern.
  • Extend EventEmitter to give custom classes their own .on()/.emit() API.
  • .emit() calls listeners synchronously, in the order they were registered.
  • The default 10-listener warning helps catch accidental listener leaks early.

Pro Tip

If you extend EventEmitter for a class that will be long-lived (like a connection pool), also provide a way to remove listeners (.off()/.removeAllListeners()) when consumers are done, otherwise listeners can silently accumulate and leak memory over the app's lifetime.