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:
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
on vs once
|
| |
|---|---|---|
Fires | Every emit | Only the first emit |
After firing | Stays subscribed | Auto-removes itself |
Use for | Repeating events ( | One-time events ( |
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 twiceRemoving 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:
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))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:
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 3000Iterating a stream of events
For repeating events, events.on() returns an async iterator — consume an open-ended stream of events with for await:
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
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')