Events & EventEmitter

1. Introduction 🔔

Node.js is built around events — from HTTP requests to stream chunks, much of the platform's internal machinery is powered by a single class: EventEmitter. This tutorial explores how events work, and how to build your own event-driven APIs.

2. What are Events? 📡

An event is a signal that something happened — a button was clicked, a file finished downloading, a connection closed. Instead of constantly checking whether something occurred, code subscribes to an event and reacts only when it's emitted.

  • Decouples the code that detects something from the code that reacts to it.
  • Supports multiple independent listeners for the same event.
  • Forms the backbone of Node's http, stream, and net modules.

3. Event-Driven Architecture 🏛️

In an event-driven architecture, the flow of a program is determined largely by events — user input, sensor output, or messages from other programs — rather than a fixed, linear sequence of instructions.

Event Source
Emits Event ("data received")
Event Loop Dispatches
Listener A runs
Listener B runs

4. The events Module 📦

Node's built-in events module (node:events) exports the EventEmitter class — the foundation of virtually all event-based APIs in the platform.

events-import.js

const EventEmitter = require('node:events');

5. EventEmitter 🎛️

EventEmitter is a class implementing the observer pattern: objects (emitters) maintain a list of listeners per event name and invoke them, synchronously and in registration order, whenever that event is emitted.

5.1 Creating an Event Emitter

create-emitter.js

const EventEmitter = require('node:events');

const emitter = new EventEmitter();

5.2 Registering Event Listeners

register-listener.js

emitter.on('userLoggedIn', (username) => {
  console.log(`${username} just logged in!`);
});

5.3 Emitting Events

emit-event.js

emitter.emit('userLoggedIn', 'alice');
// => "alice just logged in!"

Important

emit() calls all matching listeners synchronously, in the same tick — it does not defer to a future microtask or macrotask by itself.

5.4 Passing Event Arguments

Any additional arguments passed to emit() are forwarded directly to every registered listener.

event-arguments.js

emitter.on('orderPlaced', (orderId, total, currency) => {
  console.log(`Order ${orderId}: ${total} ${currency}`);
});

emitter.emit('orderPlaced', 'ORD-123', 49.99, 'USD');

5.5 One-Time Listeners

once() registers a listener that fires exactly once, then automatically removes itself.

once-example.js

emitter.once('ready', () => console.log('Initialized!'));

emitter.emit('ready'); // logs "Initialized!"
emitter.emit('ready'); // nothing happens

5.6 Removing Event Listeners

remove-listener.js

function onTick() {
  console.log('tick');
}

emitter.on('tick', onTick);
emitter.off('tick', onTick); // or emitter.removeListener('tick', onTick)

Caution

You must pass the same function reference to off() that you passed to on() — anonymous inline functions can't be individually removed.

5.7 Listener Execution Order

Listeners for a given event run in the order they were registered, synchronously, one after another.

5.8 Error Events

The special 'error' event is treated differently: if emitted without a registered listener, Node throws the error and, by default, crashes the process.

error-event.js

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

emitter.emit('error', new Error('Connection failed'));

Danger

Always attach an 'error' listener to any EventEmitter that might emit one — an unhandled 'error' event crashes the entire Node process.

6. Custom Events 🎨

6.1 Event Names

Event names are just strings (or Symbols) — there's no predefined registry. By convention, use camelCase names that describe what happened, like 'dataReceived' or 'connectionClosed'.

6.2 Multiple Listeners

A single event can have many listeners; all of them fire when the event is emitted.

multiple-listeners.js

emitter.on('fileUploaded', (name) => console.log(`Logging upload: ${name}`));
emitter.on('fileUploaded', (name) => console.log(`Sending notification for: ${name}`));

emitter.emit('fileUploaded', 'report.pdf');

7. Asynchronous Event Handling ⏱️

Listeners themselves can be async, but emit() does not wait for them to resolve — it fires all listeners and returns immediately, regardless of whether they're synchronous or asynchronous.

async-listener.js

emitter.on('save', async (data) => {
  await saveToDatabase(data); // emit() won't wait for this
});

emitter.emit('save', { id: 1 });
console.log('This logs before saveToDatabase necessarily finishes');

8. Memory Leak Warnings ⚠️

By default, Node warns if more than 10 listeners are added for a single event on one emitter — often a sign of a listener leak (e.g. registering inside a loop without ever removing them).

max-listeners.js

emitter.setMaxListeners(20); // raise the threshold if genuinely needed
console.log(emitter.getMaxListeners());

Warning

Raising maxListeners hides the symptom, not the cause — check whether listeners are being added repeatedly without cleanup first.

9. EventEmitter Methods 🧰

MethodDescription
on(event, listener)Register a listener for an event
once(event, listener)Register a listener that fires only once
off(event, listener)Remove a specific listener
emit(event, ...args)Trigger an event synchronously
removeAllListeners([event])Remove all (or all-for-one-event) listeners
listenerCount(event)Number of listeners registered for an event
eventNames()List of event names with active listeners

10. Extending EventEmitter 🏗️

The most common pattern is to subclass EventEmitter, giving a custom class built-in event capabilities.

extend-emitter.js

const EventEmitter = require('node:events');

class Downloader extends EventEmitter {
  start(url) {
    this.emit('start', url);
    // ... simulate download ...
    this.emit('progress', 50);
    this.emit('complete', url);
  }
}

const downloader = new Downloader();
downloader.on('start', (url) => console.log(`Starting: ${url}`));
downloader.on('progress', (pct) => console.log(`Progress: ${pct}%`));
downloader.on('complete', (url) => console.log(`Done: ${url}`));

downloader.start('https://example.com/file.zip');

11. Real-World Use Cases 🌍

12. Performance Considerations ⚡

  • Emitting events is cheap and synchronous — avoid heavy synchronous logic directly inside listeners on hot paths.
  • Removing unused listeners promptly avoids both memory growth and wasted CPU cycles on dead code paths.
  • Prefer once() for events that logically fire a single time, to avoid manual cleanup.

13. Best Practices ✅

  • Always attach an 'error' listener to emitters that can emit errors.
  • Use descriptive, consistent camelCase event names across your codebase.
  • Remove listeners (off) when a subscriber's lifecycle ends, to prevent leaks.
  • Keep listener functions small and focused; delegate heavy logic to separate functions.
  • Document the events a class emits and their expected argument shapes.

14. Common Mistakes ⚠️

  • Forgetting an 'error' listener, causing an unhandled crash on the first emitted error.
  • Registering listeners inside loops or repeated function calls, quietly leaking memory.
  • Expecting emit() to wait for async listeners to finish before continuing.
  • Passing a different function reference to off() than was passed to on(), so the listener never gets removed.
  • Overusing events for logic that would be clearer as a direct function call.

15. Frequently Asked Questions ❓

Question

Is emit() asynchronous?

Answer

No — emit() calls all registered listeners synchronously, in the same tick. Listeners themselves may contain asynchronous code, but emit() doesn't wait for it.

Question

What happens if I emit an event with no listeners?

Answer

Nothing happens — emit() simply returns false, except for the special 'error' event, which throws if unhandled.

Question

Can I have more than 10 listeners on one event?

Answer

Yes — the 10-listener warning is just a heuristic to catch leaks. Use setMaxListeners() to raise the limit if the behavior is genuinely intentional.

16. Summary 📝

Summary

EventEmitter is the foundation of Node's event-driven design, powering streams, servers, and sockets alike. Understanding on, once, off, and emit — plus the special handling of 'error' events — lets you build robust, decoupled APIs of your own by extending EventEmitter in custom classes.