Core 04 events
Core 04 - Events Module¶
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.md