Skip to content

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