Event Emitters
Node.js Intermediate
Chapter 2 of 8 · Event Emitters
Event Emitters
You've been using EventEmitter since Chapter 1 without knowing it — readable.on('data', ...), writable.on('error', ...), server.on('request', ...) are all EventEmitter in action. Every stream, every HTTP server, every child process in Node is built on the same EventEmitter foundation. This chapter shows you how to use it directly and build your own event-driven classes.
The Observer Pattern
EventEmitter implements the observer pattern: an object (the emitter) maintains a list of listeners and notifies them by name when something happens. The emitter doesn't know or care who is listening — it just fires the event and moves on.
Emitter
The object that fires events. Calls emit('eventName', ...args) when something noteworthy happens. Doesn't know who is listening.
Listener
A function registered with on('eventName', fn). Called synchronously each time the named event is emitted. Multiple listeners can be registered for the same event.
One-time listener
once('eventName', fn) registers a listener that auto-removes itself after firing once. Ideal for setup events: 'connect', 'open', 'ready'.
Removal
off('eventName', fn) (alias: removeListener) removes a specific listener. removeAllListeners('eventName') clears all listeners for that event.
Basic Usage
💡 emit() is Synchronous
Unlike browser DOM events, Node's emit() calls all listeners synchronously and in order before returning. If a listener throws, subsequent listeners in that emit call don't run. Keep listeners fast — defer heavy work with setImmediate() or process.nextTick() if needed.
Inspecting Listeners
Custom EventEmitter Classes
The real power comes from extending EventEmitter. This gives your class a fully event-driven API — callers subscribe to events rather than passing callbacks, which scales to multiple subscribers naturally.
The Special 'error' Event
The 'error' event is special in Node — if it fires and there is no listener registered for it, Node throws the error and crashes the process. Always attach an 'error' listener to any EventEmitter that could fail.
Memory Leaks and maxListeners
Each call to on() adds a listener. If you add listeners inside a loop or repeatedly without removing them, they accumulate. Node warns you when a single event has more than 10 listeners — this is usually a sign of a leak.
💡 prependListener() and prependOnceListener()
Listeners fire in registration order by default. If you need a listener to run before all others (e.g., an audit log that must capture the event first), use prependListener('event', fn) to insert it at the front of the queue rather than the back.
EventEmitter vs Callbacks vs Promises
Each pattern has a natural home:
| Pattern | Best for | Example |
|---|---|---|
| Callback | A single async result that happens once | fs.readFile(path, cb) |
| Promise / async-await | A single async result, with cleaner chaining | await fs.promises.readFile(path) |
| EventEmitter | Multiple events over time, multiple subscribers, ongoing state | stream.on('data', ...), server.on('request', ...) |
Practical Pattern — EventEmitter + async/await
Sometimes you need to wait for a one-time event in an async function. events.once() wraps a one-time listener in a Promise.
Quick Reference
| Method | What it does |
|---|---|
on(event, fn) | Add a persistent listener (fires every time) |
once(event, fn) | Add a listener that auto-removes after one fire |
emit(event, ...args) | Fire all listeners for this event synchronously |
off(event, fn) | Remove a specific listener (pass same function reference) |
removeAllListeners(event) | Remove all listeners for an event (or all events if omitted) |
listenerCount(event) | How many listeners are registered for this event |
eventNames() | Array of all event names that have listeners |
setMaxListeners(n) | Raise the warning threshold (default 10) |
prependListener(event, fn) | Add a listener at the front of the queue |
events.once(emitter, event) | Promise that resolves on the next emit of this event |
⚠️ Common Mistakes
Forgetting to add an 'error' listener — an unhandled 'error' event crashes the process immediately. Every custom EventEmitter that can fail needs one.
Removing the wrong function reference — off() requires the exact same function reference passed to on(). Anonymous functions and arrow functions defined inline can't be removed: ee.off('data', () => {}) does nothing because the arrow function is a new object each time.
Expecting async behaviour — emit() is synchronous. If listener A emits another event that triggers listener B, B runs before emit() returns to the caller. This can cause re-entrancy bugs in complex event graphs.
Coding Challenges
Challenge 1 — Job Queue
Build a JobQueue class that extends EventEmitter. It should accept jobs (functions) via an add(fn) method and process them one at a time with a configurable concurrency limit. Emit 'start' when a job begins (with the job index), 'done' when it completes (with the result), 'error' if it throws, and 'drain' when the queue is empty.
Challenge 2 — Typed Event Emitter
Create a TypedEmitter class that wraps EventEmitter and validates event payloads against a schema passed in the constructor. If an emitted payload fails validation, emit an 'error' instead of the original event. Define a schema for a 'userLogin' event that requires { userId: string, timestamp: number } and test it with both valid and invalid payloads.
Challenge 3 — Event Pipeline
Build three EventEmitter-based classes — DataProducer, DataTransformer, and DataConsumer — that form a pipeline: Producer emits 'data' on an interval, Transformer listens and emits transformed data, Consumer listens and logs the result. Wire them together without Producer knowing about Transformer or Consumer. Include a clean shutdown that stops the producer and emits a 'done' event that propagates through the chain.