| |

Node.js 14 🟢 The EventEmitter Class and Custom Event Handlers

The EventEmitter class is the backbone of Node.js’s event-driven architecture. It provides a simple mechanism: objects emit named events, and listeners subscribe to those events and run when they are emitted. The http.Server, net.Socket, fs.ReadStream, and process objects are all EventEmitter instances, and the pattern is available to any code that extends the class. Understanding EventEmitter is understanding how Node.js components communicate, how streams signal completion and failure, and how to build custom components that fit the ecosystem’s conventions.

The class is deliberately minimal. It has methods for adding listeners (on, once, addListener), removing them (off, removeListener, removeAllListeners), emitting events (emit), and inspecting them (listeners, listenerCount, eventNames). The power comes from the conventions around it: the error event is special, the newListener and removeListener events fire on subscription changes, and the once method provides a self-removing listener. These details matter because misusing them causes leaks, unhandled errors, and memory growth.

This chapter covers the EventEmitter class, adding and removing listeners, emitting events, the error event and its special behavior, once versus on, listener limits and memory leaks, and the patterns for building custom event emitters.

Key point: EventEmitter lets objects emit named events and lets listeners subscribe to them. The on method adds a persistent listener, and once adds a listener that removes itself after one call. The error event is special: if emitted with no listener, it throws and crashes the process. Always handle the error event when using streams, sockets, and servers. Use removeListener or off to clean up listeners and prevent memory leaks.


Why EventEmitter exists

The decoupling problem. A component that calls a callback directly is coupled to that callback. A component that emits an event is decoupled from its listeners: it announces what happened, and whoever is interested subscribes. This is the observer pattern, and it is central to Node.js’s design.

The multiple-listener problem. A callback supports one receiver. An event supports any number of listeners, each of which runs when the event fires. This is what makes streams and servers composable: a single data event can have several subscribers.

The naming problem. Events have names. A socket emits connect, data, end, error, and close. A stream emits data, end, error, close, and readable. Named events are self-documenting: the name describes what happened, and the listener describes what to do.

The lifecycle problem. Listeners can be added and removed at runtime. The once method removes itself after firing, which is the pattern for one-time events like connect and load. The removeListener method allows explicit cleanup, which matters for long-running processes that subscribe and unsubscribe repeatedly.

The ecosystem problem. The EventEmitter is a standard interface. Any object that extends it can be used wherever an emitter is expected, and tools that work with emitters work with all of them. This uniformity is why the http, net, fs, stream, and process modules all share the same event API.


a. Creating an EventEmitter

The events module exports the EventEmitter class. An instance is created with new, and a custom class extends it with extends.

const { EventEmitter } = require('events');

const emitter = new EventEmitter();

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

emitter.emit('greeting', 'Alice');  // "Hello, Alice!"

A custom class extends EventEmitter to become an emitter itself:

const { EventEmitter } = require('events');

class Job extends EventEmitter {
  constructor(name) {
    super();
    this.name = name;
  }

  start() {
    this.emit('started', this.name);
    setTimeout(() => {
      this.emit('completed', this.name, 42);
    }, 100);
  }
}

const job = new Job('import');
job.on('started', (name) => console.log(`${name} started`));
job.on('completed', (name, result) => console.log(`${name} done: ${result}`));
job.start();

The super() call initializes the EventEmitter internals, and the subclass gets on, emit, and the rest of the API.


b. Adding listeners

The on method adds a listener that stays subscribed until it is removed. The once method adds a listener that removes itself after the first invocation.

emitter.on('data', (chunk) => {
  console.log('chunk:', chunk);  // runs every time
});

emitter.once('connect', () => {
  console.log('connected');  // runs once
});

The addListener method is an alias for on:

emitter.addListener('data', handler);  // same as emitter.on

Listeners are called in the order they were added:

emitter.on('event', () => console.log('first'));
emitter.on('event', () => console.log('second'));
emitter.emit('event');
// first
// second

The on method returns the emitter, so calls can be chained:

emitter
  .on('start', onStart)
  .on('data', onData)
  .on('end', onEnd);

c. Emitting events

The emit method fires an event and invokes every listener subscribed to it. Arguments after the event name are passed to the listeners.

emitter.emit('greeting', 'Alice', 'Bob');

The listeners receive the arguments in order:

emitter.on('greeting', (a, b) => {
  console.log(a, b);  // Alice Bob
});

emit returns true if there were listeners for the event and false otherwise.

const hadListeners = emitter.emit('greeting', 'Alice');
console.log(hadListeners);  // true or false

This return value is useful for checking whether an event was handled.

Emitting an event synchronously invokes the listeners. If a listener throws, the error propagates to the caller of emit, and the remaining listeners do not run. This is why listeners should handle their own errors or be wrapped in try/catch when the event is emitted in a critical path.


d. The error event

The error event is special. If an EventEmitter emits error and there are no listeners for it, the emitter throws the error and the process crashes. This is deliberate: it surfaces silent failures that would otherwise be lost.

emitter.emit('error', new Error('something failed'));
// Throws if there is no 'error' listener

The fix is always to subscribe to error:

emitter.on('error', (err) => {
  console.error('Error:', err.message);
});

This applies to every emitter in the standard library. A net.Socket, an http.Server, a fs.ReadStream, and any custom emitter that can fail should have an error listener. Without one, the first error crashes the process.

The convention extends to functions that take a callback: if an error occurs asynchronously, the callback receives it as the first argument. For emitters, the error is emitted as an error event. The two conventions are parallel: both make errors explicit and impossible to ignore.

A subclass that emits error should document it and ensure callers know to subscribe:

class Parser extends EventEmitter {
  parse(input) {
    if (!input) {
      this.emit('error', new Error('empty input'));
      return;
    }
    // ...
  }
}

e. Removing listeners

The off method (an alias for removeListener) removes a specific listener. The removeAllListeners method removes all listeners for an event or all events.

function onData(chunk) {
  console.log(chunk);
}

emitter.on('data', onData);
emitter.off('data', onData);  // remove the listener

Removing a listener requires a reference to the same function. An inline arrow function cannot be removed because the reference is lost:

emitter.on('data', () => console.log('data'));
// Cannot remove this listener; no reference to the function

The fix is to define the listener as a named function:

function handleData(chunk) {
  console.log(chunk);
}

emitter.on('data', handleData);
emitter.off('data', handleData);  // works

removeAllListeners is useful for cleanup:

emitter.removeAllListeners('data');  // remove all 'data' listeners
emitter.removeAllListeners();        // remove all listeners

Removing listeners is important in long-running processes. A listener that is added repeatedly without being removed accumulates, which is the classic memory leak in event-driven code.


f. Listener limits and memory leaks

The EventEmitter warns when more than ten listeners are added to a single event. The warning is a heuristic: it usually indicates a leak, where listeners are added but never removed.

MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit.

The limit can be changed with setMaxListeners:

emitter.setMaxListeners(20);  // allow up to 20 listeners
emitter.setMaxListeners(0);   // unlimited

The getMaxListeners method returns the current limit, and listenerCount returns the number of listeners for an event:

console.log(emitter.getMaxListeners());
console.log(emitter.listenerCount('data'));

The warning is not an error; the listeners are still added. But it is a signal to inspect the code for a leak. The common causes are:

CauseFix
Listener added in a loopAdd once, outside the loop
Listener added per requestRemove after the request
Listener added to a shared emitterUse once or remove explicitly
Interval or timer not clearedClear on cleanup

The listeners method returns an array of the listeners for an event, and eventNames returns the names of events that have listeners:

console.log(emitter.eventNames());  // ['data', 'end']

g. The newListener and removeListener events

The EventEmitter emits newListener before a listener is added and removeListener after a listener is removed. These events are useful for tracking subscriptions, but they have subtleties.

emitter.on('newListener', (event, listener) => {
  console.log(`Listener added for ${event}`);
});

emitter.on('data', () => {});  // triggers 'newListener'

The newListener event fires before the listener is added, so listenerCount does not yet include the new listener. The removeListener event fires after the listener is removed.

These events are rarely used in application code, but they are part of the API and are sometimes used by libraries to track or log subscriptions.


Complete Example Session

// ============================================
// PART 1: BASIC EMITTER
// ============================================
const { EventEmitter } = require('events');

const emitter = new EventEmitter();

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

emitter.emit('greeting', 'Alice');  // "Hello, Alice!"
// ============================================
// PART 2: CUSTOM CLASS EXTENDING EMITTER
// ============================================
class Job extends EventEmitter {
  constructor(name) {
    super();
    this.name = name;
  }

  start() {
    this.emit('started', this.name);
    setTimeout(() => this.emit('completed', this.name, 42), 100);
  }
}

const job = new Job('import');
job.on('started', (name) => console.log(`${name} started`));
job.on('completed', (name, result) => console.log(`${name} done: ${result}`));
job.start();
// ============================================
// PART 3: ONCE VS ON
// ============================================
emitter.on('data', () => console.log('on: runs every time'));
emitter.once('connect', () => console.log('once: runs once'));

emitter.emit('data');
emitter.emit('data');
emitter.emit('connect');
emitter.emit('connect');
// on: runs every time
// on: runs every time
// once: runs once
// ============================================
// PART 4: MULTIPLE LISTENERS
// ============================================
emitter.on('event', () => console.log('first'));
emitter.on('event', () => console.log('second'));
emitter.emit('event');
// first
// second
// ============================================
// PART 5: EMIT RETURN VALUE
// ============================================
const hadListeners = emitter.emit('greeting', 'Alice');
console.log(hadListeners);  // true

const noListeners = emitter.emit('unknown');
console.log(noListeners);  // false
// ============================================
// PART 6: ERROR EVENT WITHOUT LISTENER
// ============================================
// This would throw and crash:
// emitter.emit('error', new Error('boom'));

emitter.on('error', (err) => {
  console.error('Caught:', err.message);
});

emitter.emit('error', new Error('boom'));  // Caught: boom
// ============================================
// PART 7: REMOVING LISTENERS
// ============================================
function handleData(chunk) {
  console.log('chunk:', chunk);
}

emitter.on('data', handleData);
emitter.off('data', handleData);  // removed
// ============================================
// PART 8: LISTENER COUNT
// ============================================
console.log(emitter.listenerCount('event'));  // 2
console.log(emitter.eventNames());            // ['event', ...]
// ============================================
// PART 9: MAX LISTENERS WARNING
// ============================================
emitter.setMaxListeners(20);

for (let i = 0; i < 15; i++) {
  emitter.on('data', () => {});
}
// No warning because the limit is 20
// ============================================
// PART 10: STREAMS ARE EMITTERS
// ============================================
const fs = require('fs');

const stream = fs.createReadStream('large.txt', 'utf8');

stream.on('data', (chunk) => process.stdout.write('.'));
stream.on('end', () => console.log('\ndone'));
stream.on('error', (err) => console.error('stream error:', err));

These ten parts cover a basic emitter, a custom class extending EventEmitter, once versus on, multiple listeners, the emit return value, the error event, removing listeners, listener count, the max listeners warning, and streams as emitters.


Quick Reference

Methods

MethodPurpose
on(event, listener)Add a persistent listener
once(event, listener)Add a one-time listener
off(event, listener)Remove a listener
addListener(event, listener)Alias for on
removeListener(event, listener)Alias for off
removeAllListeners([event])Remove all listeners
emit(event, ...args)Fire an event
listenerCount(event)Number of listeners
listeners(event)Array of listeners
eventNames()Names of events with listeners
setMaxListeners(n)Set the listener limit
getMaxListeners()Get the listener limit

Special Events

EventWhen it fires
newListenerBefore a listener is added
removeListenerAfter a listener is removed
errorEmitted with an error; throws if no listener

on vs once

Aspectononce
FiresEvery timeOnce
Removes itselfNoYes
Use forRecurring eventsOne-time events

Listener Limit

SettingEffect
Default10 listeners per event
setMaxListeners(n)Change the limit
setMaxListeners(0)Unlimited
WarningMaxListenersExceededWarning

Common Emitters

EmitterEvents
http.Serverrequest, listening, error
net.Socketconnect, data, end, error, close
fs.ReadStreamdata, end, error, close
processexit, uncaughtException, SIGINT
ChildProcessexit, close, error, message

Best Practices

✅ Do This:

// Always handle 'error'
stream.on('error', (err) => console.error(err));

// Use once for one-time events
emitter.once('connect', () => console.log('connected'));

// Use named functions for removable listeners
function onData(chunk) { console.log(chunk); }
emitter.on('data', onData);
emitter.off('data', onData);

// Remove listeners on cleanup
emitter.removeAllListeners();

// Extend EventEmitter for custom components
class Worker extends EventEmitter { }

❌ Don’t Do This:

// Ignore the error event
stream.on('data', handler);  // ❌ no error handler

// Use inline functions for removable listeners
emitter.on('data', () => {});  // ❌ cannot remove

// Add listeners in a loop without removing
for (let i = 0; i < 100; i++) {
  emitter.on('data', handler);  // ❌ leak
}

// Emit 'error' without a listener
emitter.emit('error', new Error('boom'));  // ❌ crashes

Common Pitfalls

PitfallWhy It HappensFix
Process crashes on errorNo error listenerAdd on('error', ...)
Cannot remove listenerInline functionUse a named function
Memory leak warningListeners added in a loopAdd once or remove
Listener runs multiple timesUsed on instead of onceUse once
Listeners run in unexpected orderAssumed orderListeners run in add order
Event emitted with no listenerSilent no-opCheck emit return value

Real-World Examples

1. Basic Emitter

const emitter = new EventEmitter();
emitter.on('event', handler);
emitter.emit('event', arg);

2. Custom Class

class Downloader extends EventEmitter {
  start() {
    this.emit('start');
    // ...
    this.emit('done');
  }
}

3. Error Handling

stream.on('error', (err) => console.error(err));

4. Once for Connect

socket.once('connect', () => console.log('connected'));

5. Remove Listener

function handler(data) { }
emitter.on('data', handler);
emitter.off('data', handler);

6. Remove All

emitter.removeAllListeners('data');

7. Listener Count

console.log(emitter.listenerCount('data'));

8. Increase Limit

emitter.setMaxListeners(50);

9. Check Emit Result

if (!emitter.emit('event')) {
  console.log('no listeners');
}

10. Process Events

process.on('SIGINT', () => {
  console.log('shutting down');
  process.exit(0);
});

Visual

EventEmitter Flow

┌──────────────────────────────────────────────────────────────┐
│  EMITTER                                                     │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  emit('data', chunk)                                   │  │
│  └────────────────────────┬───────────────────────────────┘  │
│                           │                                  │
│                           ▼                                  │
│  LISTENERS                                                   │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  listener 1 (on)     →  runs every time                │  │
│  │  listener 2 (on)     →  runs every time                │  │
│  │  listener 3 (once)   →  runs once, then removed        │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

on vs once

┌──────────────────────────────────────────────────────────────┐
│  emitter.on('event', handler)                                │
│  ├── emit 1 → handler runs                                   │
│  ├── emit 2 → handler runs                                   │
│  └── emit 3 → handler runs                                   │
│                                                              │
│  emitter.once('event', handler)                              │
│  ├── emit 1 → handler runs                                   │
│  ├── emit 2 → (listener removed)                             │
│  └── emit 3 → (listener removed)                             │
└──────────────────────────────────────────────────────────────┘

Error Event

┌──────────────────────────────────────────────────────────────┐
│  NO ERROR LISTENER:                                          │
│  emitter.emit('error', new Error('boom'))                    │
│  └── Throws, process crashes                                 │
│                                                              │
│  WITH ERROR LISTENER:                                        │
│  emitter.on('error', (err) => console.error(err))            │
│  emitter.emit('error', new Error('boom'))                    │
│  └── Listener runs, process continues                        │
└──────────────────────────────────────────────────────────────┘

Memory Leak from Listeners

┌──────────────────────────────────────────────────────────────┐
│  BAD:                                                        │
│  function handleRequest() {                                  │
│    emitter.on('data', handler);  // ❌ added per request     │
│  }                                                           │
│  └── Listeners accumulate                                    │
│                                                              │
│  GOOD:                                                       │
│  emitter.on('data', handler);  // added once at startup      │
│                                                              │
│  OR:                                                         │
│  function handleRequest() {                                  │
│    emitter.once('data', handler);  // removed after firing   │
│  }                                                           │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
EventEmitterClass for emitting and listening to events
onPersistent listener
onceOne-time listener
off / removeListenerRemove a listener
removeAllListenersRemove all listeners
emitFire an event
error eventThrows if no listener
Listener limit10 per event by default
setMaxListenersChange the limit
listenerCountNumber of listeners
eventNamesNames of events with listeners
StreamsAre EventEmitter instances

Key takeaways:

  • EventEmitter is the standard mechanism for events in Node.js. Objects emit named events, and listeners subscribe. The pattern decouples the emitter from the listeners and supports multiple subscribers.
  • The error event is special. If emitted with no listener, it throws and crashes the process. Always subscribe to error on streams, sockets, servers, and any emitter that can fail.
  • on adds a persistent listener; once adds a one-time listener. once removes itself after firing, which is the pattern for one-time events like connect and load.
  • Removing a listener requires a reference to the same function. Inline arrow functions cannot be removed because the reference is lost. Use named functions for listeners that need to be removed.
  • The max listeners warning signals a possible leak. More than ten listeners on a single event triggers a warning. The common causes are adding listeners in a loop or per request without removing them.
  • emit returns a boolean. It returns true if there were listeners for the event and false otherwise. This is useful for checking whether an event was handled.
  • Streams, sockets, and servers are emitters. The http.Server, net.Socket, fs.ReadStream, and process objects all extend EventEmitter, and their events follow the same conventions.

Remember: The EventEmitter is the backbone of Node.js’s event-driven architecture. It provides a simple, uniform mechanism for objects to announce events and for listeners to subscribe. The error event is the one that must always be handled, because an unhandled error event crashes the process. The once method is the pattern for one-time events, and removeListener is the pattern for cleanup. The max listeners warning is a signal to inspect for leaks. Understanding the EventEmitter is understanding how Node.js components communicate, how streams signal completion and failure, and how to build custom components that fit the ecosystem’s conventions.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!