The Node Event emitter

By

Learn how to work with custom events in Node.js using the EventEmitter class from node:events, with the on, emit, once, off and removeListener methods.

~~~

If you worked with JavaScript in the browser, you know how much of the interaction of the user is handled through events: mouse clicks, keyboard button presses, reacting to mouse movements, and so on.

On the backend side, Node.js offers us the option to build a similar system using the events module.

This module, in particular, offers the EventEmitter class, which we’ll use to handle our events.

You initialize an EventEmitter object using this syntax:

const EventEmitter = require('node:events')
const eventEmitter = new EventEmitter()

The node: prefix makes it clear you’re loading a module built into Node, not a package from npm. require('events') still works, but the Node docs use the prefix.

If your project uses ES modules, import the class instead:

import { EventEmitter } from 'node:events'

const eventEmitter = new EventEmitter()

The rest of the code in this post is the same either way.

This object exposes, among many others, the on and emit methods.

Emit and listen for events

For example, let’s create a start event, and as a matter of providing a sample, we react to that by just logging to the console:

eventEmitter.on('start', () => {
  console.log('started')
})

When we run

eventEmitter.emit('start')

the event handler function is triggered, and we get the console log.

addListener() is an alias for on(), in case you see that used.

Passing arguments to the event

You can pass arguments to the event handler by passing them as additional arguments to emit():

eventEmitter.on('start', (number) => {
  console.log(`started ${number}`)
})

eventEmitter.emit('start', 23)

Multiple arguments:

eventEmitter.on('start', (start, end) => {
  console.log(`started from ${start} to ${end}`)
})

eventEmitter.emit('start', 1, 100)

Listen for an event just once

The EventEmitter object also exposes the once() method, which you can use to create a one-time event listener.

Once that event is fired, the listener stops listening.

Example:

eventEmitter.once('start', () => {
  console.log(`started!`)
})

eventEmitter.emit('start')
eventEmitter.emit('start') //not going to fire

Removing an event listener

Once you create an event listener, you can remove it using the removeListener() method.

To do so, we must first have a reference to the callback function of on.

In this example:

eventEmitter.on('start', () => {
  console.log('started')
})

Extract the callback:

const callback = () => {
  console.log('started')
}

eventEmitter.on('start', callback)

So that later you can call

eventEmitter.removeListener('start', callback)

off() is an alias for removeListener(), so this does the same thing:

eventEmitter.off('start', callback)

You can also remove all listeners at once on an event, using:

eventEmitter.removeAllListeners('start')

Getting the events registered

The eventNames() method, called on an EventEmitter object instance, returns an array with the names of the events that have listeners. Each name is a string, or a Symbol if you used a Symbol as the event name:

const EventEmitter = require('node:events')
const eventEmitter = new EventEmitter()

eventEmitter.on('start', () => {
  console.log('started')
})

eventEmitter.eventNames() // [ 'start' ]

listenerCount() returns the count of listeners of the event passed as parameter:

eventEmitter.listenerCount('start') //1

Adding more listeners before/after other ones

If you have multiple listeners, the order of them might be important.

An EventEmitter object instance offers some methods to work with order.

emitter.prependListener()

When you add a listener using on or addListener, it’s added last in the queue of listeners, and called last. Using prependListener it’s added, and called, before other listeners.

Listeners run one after the other, in the order they sit in the queue. Here we add a listener with on() first, then one with prependListener():

eventEmitter.on('start', () => {
  console.log('second')
})

eventEmitter.prependListener('start', () => {
  console.log('first')
})

eventEmitter.emit('start')

The output is:

first
second

The prepended listener jumps to the front of the line, even though we added it later.

This is useful when you don’t control the code that registered the other listeners, like a library, and you need your code to run before theirs.

emitter.prependOnceListener()

When you add a listener using once, it’s added last in the queue of listeners, and called last. Using prependOnceListener it’s added, and called, before other listeners.

It’s the same idea as prependListener(), but the listener runs only the first time:

eventEmitter.on('start', () => {
  console.log('regular listener')
})

eventEmitter.prependOnceListener('start', () => {
  console.log('setup, only once')
})

eventEmitter.emit('start')
eventEmitter.emit('start')

The output is:

setup, only once
regular listener
regular listener

On the first emit(), the prepended listener runs first and then removes itself. On the second one, only the regular listener is left.

The error event

One event name is special: error.

If you emit an error event and nobody listens for it, Node.js throws the error and your program crashes:

eventEmitter.emit('error', new Error('Could not connect'))
// Error: Could not connect
// ...the process exits

Once you add a listener for error, Node hands the error to it instead of throwing:

eventEmitter.on('error', (err) => {
  console.error(`Something went wrong: ${err.message}`)
})

eventEmitter.emit('error', new Error('Could not connect'))
// Something went wrong: Could not connect

If your code emits error events, or you use a Node object that does (like a stream or a server), add an error listener.

Extending EventEmitter in your own class

So far we used a plain eventEmitter object. In real code, you’ll more often see a class that extends EventEmitter.

This way your object can announce what’s happening, and other parts of your program decide what to do about it. Many Node APIs work like this. Streams, HTTP servers and child processes are all EventEmitters.

Here’s a small Download class that fakes a download and emits start, progress and done events:

import { EventEmitter } from 'node:events'

class Download extends EventEmitter {
  constructor(url) {
    super()
    this.url = url
  }

  start() {
    this.emit('start', this.url)

    let percent = 0
    const timer = setInterval(() => {
      percent += 25
      this.emit('progress', percent)

      if (percent === 100) {
        clearInterval(timer)
        this.emit('done')
      }
    }, 100)
  }
}

const download = new Download('https://flaviocopes.com/rss.xml')

download.on('start', (url) => console.log(`downloading ${url}`))
download.on('progress', (percent) => console.log(`${percent}%`))
download.on('done', () => console.log('finished'))

download.start()

The output is:

downloading https://flaviocopes.com/rss.xml
25%
50%
75%
100%
finished

Remember to call super() in the constructor before you use this. If you forget it, new Download() throws a ReferenceError.

Notice that the class never calls console.log. It emits events, and the code using it decides what to do with them.

Watch out for piling up listeners

Every on() call adds a new listener instead of replacing the old one.

If you add a listener inside something that runs many times, like a function called on every request, listeners pile up. Each emit() then runs all of them, and they’re never cleaned up.

Node.js warns you when an event gets more than 10 listeners:

for (let i = 0; i < 11; i++) {
  eventEmitter.on('request', () => {})
}
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 request listeners added to [EventEmitter]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limit

It’s only a warning, and the listeners still work. Most of the time it points to a bug, though. Remove the listener with off() when you’re done with it, or use once() if you need it just one time.

If you really need more listeners, raise the limit:

eventEmitter.setMaxListeners(20)
Tagged: Node.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about node: