EventEmitter
Event-driven programming is central to Node.js. The EventEmitter class from the events module is the mechanism behind streams, HTTP servers, sockets, and many third-party libraries. It lets objects emit named events and register listeners that respond to them.
While callbacks and promises handle one-off asynchronous operations, EventEmitters are ideal for ongoing, multi-step processes where multiple parts of the application need to react to state changes.
This guide explains the EventEmitter API, listener management, error handling, memory leak detection, and common design patterns for building maintainable event-based systems.
Create an EventEmitter instance, register listeners with on or addListener, and emit events with emit. Listeners are called synchronously in the order they were added.
| 1 | const EventEmitter = require('events'); |
| 2 | |
| 3 | const emitter = new EventEmitter(); |
| 4 | |
| 5 | emitter.on('ping', (data) => { |
| 6 | console.log('ping received:', data); |
| 7 | }); |
| 8 | |
| 9 | emitter.emit('ping', { time: Date.now() }); |
| 10 | |
| 11 | // Output: |
| 12 | // ping received: { time: 1234567890 } |
The once method registers a listener that is removed after the first invocation. This is useful for one-time lifecycle events like ready or open.
| 1 | emitter.once('ready', () => { |
| 2 | console.log('System is ready'); |
| 3 | }); |
| 4 | |
| 5 | emitter.emit('ready'); |
| 6 | emitter.emit('ready'); // second emit does nothing |
info
Most real-world usage extends EventEmitter in a custom class. This lets you combine event emission with business logic and state. Remember to call super() in the constructor.
| 1 | const EventEmitter = require('events'); |
| 2 | |
| 3 | class JobQueue extends EventEmitter { |
| 4 | constructor() { |
| 5 | super(); |
| 6 | this.jobs = []; |
| 7 | this.running = false; |
| 8 | } |
| 9 | |
| 10 | add(job) { |
| 11 | this.jobs.push(job); |
| 12 | this.emit('job:added', job); |
| 13 | this.process(); |
| 14 | } |
| 15 | |
| 16 | async process() { |
| 17 | if (this.running) return; |
| 18 | this.running = true; |
| 19 | |
| 20 | while (this.jobs.length > 0) { |
| 21 | const job = this.jobs.shift(); |
| 22 | this.emit('job:started', job); |
| 23 | try { |
| 24 | await job.run(); |
| 25 | this.emit('job:completed', job); |
| 26 | } catch (err) { |
| 27 | this.emit('job:failed', job, err); |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | this.running = false; |
| 32 | this.emit('idle'); |
| 33 | } |
| 34 | } |
best practice
If an EventEmitter emits an error event and there are no listeners for it, Node.js throws the error and may crash the process. Always attach an error listener.
| 1 | const EventEmitter = require('events'); |
| 2 | const emitter = new EventEmitter(); |
| 3 | |
| 4 | emitter.on('error', (err) => { |
| 5 | console.error('Emitter error:', err.message); |
| 6 | }); |
| 7 | |
| 8 | emitter.emit('error', new Error('Something went wrong')); |
| 9 | // Process stays alive |
For application-level error handling, consider using an error event as the channel for non-fatal failures. Fatal errors should still propagate to a global handler or crash the process after cleanup.
warning
Adding listeners without removing them keeps emitter objects alive. This is especially dangerous when emitters are long-lived and listeners are registered repeatedly, such as inside request handlers.
| 1 | // BAD: adds a new listener on every request |
| 2 | app.get('/data', (req, res) => { |
| 3 | const emitter = getGlobalEmitter(); |
| 4 | emitter.on('update', (data) => res.json(data)); |
| 5 | }); |
| 1 | // GOOD: remove listener when response finishes |
| 2 | app.get('/data', (req, res) => { |
| 3 | const emitter = getGlobalEmitter(); |
| 4 | |
| 5 | function onUpdate(data) { |
| 6 | res.json(data); |
| 7 | emitter.off('update', onUpdate); |
| 8 | } |
| 9 | |
| 10 | emitter.on('update', onUpdate); |
| 11 | req.on('close', () => emitter.off('update', onUpdate)); |
| 12 | }); |
EventEmitter warns when more than 10 listeners are attached to a single event. You can change this with setMaxListeners, but a high number usually indicates a leak.
best practice
Request/Response correlation
When emitting a request event, include a correlation ID so the response can be matched to the original caller.
| 1 | const id = crypto.randomUUID(); |
| 2 | emitter.once(`response:${id}`, (result) => { |
| 3 | console.log('Got response', result); |
| 4 | }); |
| 5 | emitter.emit('request', { id, payload }); |
Event namespaces
Use colon-separated namespaces for clarity: user:created, user:updated, user:deleted.
Async listeners
EventEmitter listeners are synchronous by design. If you need async handling, either await inside the listener or emit a promise-returning function.
note
The events module provides once(emitter, event), which returns a promise that resolves the next time the event fires. This bridges EventEmitters and promise-based code.
| 1 | const { once } = require('events'); |
| 2 | const http = require('http'); |
| 3 | |
| 4 | const server = http.createServer(); |
| 5 | server.listen(3000); |
| 6 | |
| 7 | await once(server, 'listening'); |
| 8 | console.log('Server is listening'); |
info
| Mistake | Consequence | Fix |
|---|---|---|
| No error listener | Process crash | Always handle error events |
| Leaked listeners | Memory growth | Use once and remove listeners |
| Synchronous emit assumptions | Unexpected ordering | Document listener execution order |
| Too many events | Hard to debug | Use namespaces and typed schemas |