Core 04 - Events Module
Table of Contents
- Basic Idea
- EventEmitter: The Backbone
- Essential Methods
- Event Names Convention
- maxListeners Warning
- Error Events - Process Crasher
- EventEmitter vs Callbacks vs Promises
- Real Example: Custom Download Manager
- Memory Leak Prevention
- Summary
- Prerequisites
Basic Idea
Node’s entire async architecture is built on events
HTTP requests , socket data , file reads - everything emits events
EventEmitter is the pattern behind it all
EventEmitter: The Backbone
const EventEmitter = require('events')
const emitter = new EventEmitter()
// subscribe
emitter.on('data', (payload) => {
console.log('received:', payload)
})
// emit
emitter.emit('data', { id: 1, message: 'hello' })
Every Node stream , http server , net socket extends EventEmitter under the hood
When you do stream.on('data', handler) , you’re using the same API
Essential Methods
const emitter = new EventEmitter()
// on - add listener
emitter.on('request', handler)
// once - fires exactly once then removes itself
emitter.once('connection', () => {
console.log('first connection - logging only once')
})
// off - remove specific listener
emitter.off('request', handler)
// removeAllListeners - clean slate
emitter.removeAllListeners('request')
// emit - fire event (synchronous by default)
const handled = emitter.emit('request', reqData)
// returns false if nobody is listening
// listenerCount - check subscribers
console.log(emitter.listenerCount('request')) // 0 after removal
once() is perfect for one-time setup
off() requires the same function reference - anonymous callbacks can’t be removed
listenerCount() is useful for debugging memory leaks
Event Names Convention
// camelCase for event names
emitter.emit('userLogin', userId)
// Node internals use camelCase too
stream.on('data', ...)
stream.on('end', ...)
stream.on('error', ...)
There’s no formal rule but Node and the ecosystem all use camelCase Don’t use spaces or special characters in event names
maxListeners Warning
const emitter = new EventEmitter()
// add 11+ listeners to same event - Node prints a warning
for (let i = 0; i < 15; i++) {
emitter.on('data', () => {})
}
// (node) warning: possible EventEmitter memory leak detected.
// 15 data listeners added to [EventEmitter]. Use emitter.setMaxListeners()
// fix - increase limit or find the leak
emitter.setMaxListeners(20)
// better: figure out why you need 20 listeners
Default is 10 listeners per event type If you need more , either increase it or investigate why you’re accumulating handlers Most leaks come from classes that create emitters but never clean up
Error Events - Process Crasher
const emitter = new EventEmitter()
// if 'error' is emitted and nobody is listening - process crashes
emitter.emit('error', new Error('something broke'))
// throws and crashes the process
// always listen for error
emitter.on('error', (err) => {
console.error('emitter error:', err.message)
// handle gracefully instead of crash
})
// best practice: always attach error handler immediately
const stream = require('fs').createReadStream('nonexistent.txt')
stream.on('error', (err) => {
// file not found - handle it
})
An unhandled 'error' event on an EventEmitter crashes the process
Always register error handlers before anything else
This is the #1 cause of “my Node app crashed and I don’t know why”
EventEmitter vs Callbacks vs Promises
// callbacks - one shot, no reuse
readFile('config.json', (err, data) => {
if (err) return handleError(err)
process(data)
})
// promises - one shot, composable
const data = await readFile('config.json')
// events - multiple firings, real-time
sensor.on('temperature', (temp) => {
display.update(temp) // fires repeatedly
})
Callbacks and promises resolve once - for a single async operation Events are for things that happen multiple times over time Use the right tool: don’t wrap HTTP request handlers in promises (events are correct there)
Real Example: Custom Download Manager
const EventEmitter = require('events')
class DownloadManager extends EventEmitter {
constructor() {
super()
this.activeDownloads = 0
}
start(url) {
this.activeDownloads++
this.emit('start', url)
// simulate download
const total = 100
let received = 0
const interval = setInterval(() => {
received += 10
this.emit('progress', { url, received, total })
if (received >= total) {
clearInterval(interval)
this.activeDownloads--
this.emit('complete', url)
if (this.activeDownloads === 0) {
this.emit('drain')
}
}
}, 200)
}
}
const dm = new DownloadManager()
dm.on('start', (url) => console.log('started:', url))
dm.on('progress', ({ url, received, total }) => {
console.log(`${url}: ${received}/${total}`)
})
dm.on('drain', () => console.log('all downloads done'))
dm.start('https://example.com/file.zip')
Memory Leak Prevention
class ConnectionPool {
constructor() {
this.emitter = new EventEmitter()
}
addConnection(id) {
const handler = () => {
console.log(`connection ${id} data received`)
}
this.emitter.on('data', handler)
// store reference for cleanup
this.emitter._handlers = this.emitter._handlers || new Map()
this.emitter._handlers.set(id, handler)
}
removeConnection(id) {
const handler = this.emitter._handlers?.get(id)
if (handler) {
this.emitter.off('data', handler)
this.emitter._handlers.delete(id)
}
}
destroy() {
this.emitter.removeAllListeners()
}
}
Every listener holds a reference to the callback function If you add listeners in a loop without cleanup , those callbacks (and their closures) leak memory Always store references if you need to remove listeners later
Summary
- EventEmitter is everywhere in Node - streams , HTTP , sockets
- Always handle
'error'events or your process crashes once()for one-shot ,on()for repeaters- maxListeners warning means a leak - investigate don’t blindly increase
- Clean up listeners when classes are destroyed
Prerequisites
next -> core_05_buffers
Posted · Updated