NodeJSEmitting & Listening to Events

Emitting & Listening to Events

This page is the hands-on companion to the EventEmitter class: how to fire events with data, subscribe in the right way, handle one-shot versus repeating events, and — crucially — remove listeners so you don't leak memory. These are the everyday moves you'll repeat in every Node codebase.

Emitting with data

Every argument after the event name is passed straight to each listener. There's no limit and no serialization — listeners receive the actual objects:

JS
import { EventEmitter } from 'node:events'
const orders = new EventEmitter()

orders.on('placed', (order, user) => {
  console.log(`${user.name} ordered ${order.item} for $${order.price}`)
})

orders.emit('placed', { item: 'Book', price: 25 }, { name: 'Ada' })
Ada ordered Book for $25
Convention: pass one object, not many positionals
As your events grow, a single payload object — `emit('placed', { order, user })` — beats a long positional list. Listeners destructure what they need and you can add fields later without breaking existing handlers' argument positions. It also self-documents the payload shape.
on vs once

on

once

Fires

Every emit

Only the first emit

After firing

Stays subscribed

Auto-removes itself

Use for

Repeating events (data, request)

One-time events (ready, connect, end)

JS
const db = new EventEmitter()

db.once('connected', () => console.log('connect once'))   // runs a single time
db.on('query', (sql) => console.log('query:', sql))        // runs every time

db.emit('connected'); db.emit('connected')   // logs once
db.emit('query', 'SELECT 1'); db.emit('query', 'SELECT 2') // logs twice
Removing listeners

To remove a listener you need a reference to the same function — which means anonymous inline functions can never be removed. Name them, or keep the reference:

JS
const e = new EventEmitter()

const onData = (chunk) => console.log(chunk)
e.on('data', onData)
// ... later
e.off('data', onData)        // works — same reference

// ✗ This can NEVER be removed — no handle to the function:
e.on('data', (chunk) => console.log(chunk))
Anonymous listeners are unremovable — and leak
`e.on('data', () => {...})` gives you no way to ever `off` it. In long-lived processes, adding anonymous listeners (e.g. one per request to a shared emitter) accumulates them forever — a textbook memory leak that also trips the `MaxListenersExceededWarning`. Keep a named reference for anything you might need to detach, and prefer `once` for one-shot subscriptions so cleanup is automatic.
Awaiting an event

events.once() (the standalone helper, not the method) turns a one-shot event into a promise — letting you await it in linear code instead of nesting callbacks:

JS
import { once, EventEmitter } from 'node:events'

const server = new EventEmitter()
setTimeout(() => server.emit('ready', 3000), 50)

const [port] = await once(server, 'ready')   // resolves with emit's args
console.log('ready on', port)                 // → ready on 3000
`once()` also rejects on `error`
The promise from `events.once(emitter, 'ready')` automatically *rejects* if the emitter fires `'error'` before `'ready'` — so a single `try/catch` covers both the success and failure paths. This is the cleanest bridge from the event world to async/await.
Iterating a stream of events

For repeating events, events.on() returns an async iterator — consume an open-ended stream of events with for await:

JS
import { on, EventEmitter } from 'node:events'

const ticker = new EventEmitter()
let n = 0
const id = setInterval(() => ticker.emit('tick', ++n), 100)

for await (const [count] of on(ticker, 'tick')) {
  console.log('tick', count)
  if (count === 3) { clearInterval(id); break }   // break to stop iterating
}
tick 1
tick 2
tick 3
A complete, well-behaved emitter

JS
import { EventEmitter } from 'node:events'

class Downloader extends EventEmitter {
  async fetch(url) {
    this.emit('start', url)
    try {
      for (let pct = 0; pct <= 100; pct += 50) this.emit('progress', pct)
      this.emit('done', { url, bytes: 1024 })
    } catch (err) {
      this.emit('error', err)        // always provide an error path
    }
  }
}

const d = new Downloader()
d.on('start', (u) => console.log('start', u))
d.on('progress', (p) => console.log(p + '%'))
d.once('done', (r) => console.log('done', r.bytes, 'bytes'))
d.on('error', (e) => console.error('failed', e.message))
d.fetch('https://example.com/file')
Next
Level up with the reusable designs the ecosystem relies on: [EventEmitter Patterns](/nodejs/event-emitter-patterns).