Module F-9·14 min read

The EventEmitter class, on/emit/once/removeListener, custom events, why streams and HTTP are built on EventEmitter, and memory leak prevention.

Written by Jatin Jain Saraf · Architect of SupraScan

Module F-9 — Event Emitters & Node's Event-Driven Core

What this module covers: The EventEmitter class is the foundation of Node.js's architecture. HTTP servers, file streams, database connections, WebSockets — they all inherit from EventEmitter. You have already used it without knowing it: every time you called server.listen() or req.on('data', ...), you were using an EventEmitter. This module explains how EventEmitter works, how to build your own, how to avoid the memory leak that trips up every developer, and how the pattern underpins Node.js internals. Understanding this bridges Foundation to everything in the Practitioner and Architect phases.


What Is an Event Emitter?

An EventEmitter is an object that:

  1. Lets you register named listeners — functions to call when a specific event occurs
  2. Lets you emit events by name — which synchronously calls all registered listeners

It is the observer pattern, built into the Node.js runtime.

An EventEmitter is a radio station, not a phone call — it broadcasts on a named frequency without knowing or caring who's tuned in. A Promise is a phone call — one caller, one answer, then it's over.

import { EventEmitter } from 'node:events';
 
const emitter = new EventEmitter();
 
// Register a listener for the 'greet' event
emitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});
 
// Emit the event — calls all registered 'greet' listeners
emitter.emit('greet', 'Jatin');  // → Hello, Jatin!
emitter.emit('greet', 'World'); // → Hello, World!

Simple enough. The power comes from the fact that the emitter and the listener are decoupled — the code that fires the event does not know who is listening, and the listener does not know when or why the event was fired.


Core EventEmitter API

import { EventEmitter } from 'node:events';
 
const emitter = new EventEmitter();
 
// on() — listen for every occurrence of an event
emitter.on('data', (chunk) => {
  console.log('Received:', chunk);
});
 
// once() — listen only once, then automatically remove the listener
emitter.once('connected', () => {
  console.log('Connected to database');
});
 
// emit() — fire an event with optional arguments
emitter.emit('data', Buffer.from('hello'));
emitter.emit('connected');
emitter.emit('connected'); // second emit — the 'once' listener is gone
 
// off() / removeListener() — remove a specific listener
function onError(err) {
  console.error('Error:', err.message);
}
 
emitter.on('error', onError);
emitter.off('error', onError);   // remove it later
 
// removeAllListeners() — remove all listeners for an event (or all events)
emitter.removeAllListeners('data');
emitter.removeAllListeners();    // clear everything
 
// listenerCount() — how many listeners are registered for an event
console.log(emitter.listenerCount('data')); // 0
 
// eventNames() — list all events that have listeners
console.log(emitter.eventNames());

Building Your Own EventEmitter Subclass

The real pattern: extend EventEmitter in your own classes. This is how Node.js HTTP servers, streams, and virtually every I/O object are built.

import { EventEmitter } from 'node:events';
 
class DataPipeline extends EventEmitter {
  #isRunning = false;
  #processed = 0;
 
  start() {
    if (this.#isRunning) return;
    this.#isRunning = true;
 
    this.emit('started');
 
    // Simulate processing records
    const interval = setInterval(() => {
      this.#processed++;
      const record = { id: this.#processed, value: Math.random() };
 
      this.emit('record', record);  // emit each record
 
      if (this.#processed >= 5) {
        clearInterval(interval);
        this.#isRunning = false;
        this.emit('finished', this.#processed);
      }
    }, 100);
  }
 
  stop() {
    this.#isRunning = false;
    this.emit('stopped');
  }
}
 
// Usage
const pipeline = new DataPipeline();
 
pipeline.on('started', () => console.log('Pipeline started'));
pipeline.on('record', (record) => console.log('Processing:', record));
pipeline.on('finished', (count) => console.log(`Done. Processed ${count} records.`));
 
pipeline.start();

Output:

Pipeline started
Processing: { id: 1, value: 0.4521... }
Processing: { id: 2, value: 0.8734... }
...
Done. Processed 5 records.

The caller does not need to know anything about the pipeline's internals. It just listens for named events.


The error Event Is Special

If an EventEmitter emits an 'error' event and there is no listener registered for it, Node.js throws the error and crashes the process. This is not optional — it is a design decision to force you to handle errors explicitly.

const emitter = new EventEmitter();
 
// ❌ This will crash the process
emitter.emit('error', new Error('Something went wrong'));
// → Uncaught Error: Something went wrong
 
// ✅ Always register an error listener
emitter.on('error', (err) => {
  console.error('Emitter error:', err.message);
});
 
emitter.emit('error', new Error('Something went wrong'));
// → Emitter error: Something went wrong

Any class extending EventEmitter should either:

  1. Register a default 'error' listener in the constructor, or
  2. Document clearly that callers must register one

The Memory Leak Warning

This is the single most common EventEmitter mistake. By default, Node.js warns if you register more than 10 listeners for the same event on the same emitter:

MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 'data' listeners added to [EventEmitter].

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.