Understanding node's possible eventemitter leak error message April 25, 2015

In node.js and io.js, you'll eventually see this error message:

(node) warning: possible EventEmitter memory leak detected. 11 a listeners added. Use emitter.setMaxListeners() to increase limit.

When would a leak actually occur?

A leak occurs when you continuously add event handlers without removing them. This particular happens when you use a single emitter instance numerous times. Let's make a function that returns the next value in a stream:

function next(stream) {
  // if the stream has data buffered, return that
  {
    let data = stream.read()
    if (data) return Promise.resolve(data)
  }

  // if the stream has already ended, return nothing
  if (!data.readable) return Promise.resolve(null)

  // wait for data
  return new Promise(function (resolve, reject) {
    stream.once('readable', () => resolve(stream.read()))
    stream.on('error', reject)
    stream.on('end', resolve)
  })
}

Every time you call next() on stream, you add a handler on readable, error, and end. On the 11th next(stream) call, you'll get the error message:

(node) warning: possible EventEmitter memory leak detected. 11 a listeners added. Use emitter.setMaxListeners() to increase limit.

You've continuously added handlers to error and end, but have not removed them, even if data was successfully read and those handlers are no longer relevant.

Cleaning up your event handlers

The correct way to clean up your handlers is to make sure that after the promise resolves, a net of 0 event handlers are added:

return new Promise(function (resolve, reject) {
  stream.on('readable', onreadable)
  stream.on('error', onerror)
  stream.on('end', cleanup)

  // define all functions in scope
  // so they can be referenced by cleanup and vice-versa
  function onreadable() {
    cleanup()
    resolve(stream.read())
  }

  function onerror(err) {
    cleanup()
    reject(err)
  }

  function cleanup() {
    // remove all event listeners created in this promise
    stream.removeListener('readable', onreadable)
    stream.removeListener('error', onerror)
    stream.removeListener('end', cleanup)
  }
})

With this method, there will be no event emitter leak as after every promise resolves, the net change events handlers is 0.

Concurrent handlers

What if you want multiple listeners on the same emitter? For example, you may have a lot of functions listening to the same emitter:

doThis1(stream)
doThis2(stream)
doThis3(stream)
doThis4(stream)
doThis5(stream)
doThis6(stream)
doThis7(stream)
doThis8(stream)
doThis9(stream)
doThis10(stream)
doThis11(stream)
doThis12(stream)
doThis13(stream)

If all the functions above add handlers to the data event, you're going to get the same leak error message, but you know there isn't an actual leak. At this point, you should set the maximum number of listeners accordingly:

return new Promise(function (resolve, reject) {
  // increase the maximum number of listeners by 1
  // while this promise is in progress
  stream.setMaxListeners(stream.getMaxListeners() + 1)
  stream.on('readable', onreadable)
  stream.on('error', onerror)
  stream.on('end', cleanup)

  function onreadable() {
    cleanup()
    resolve(stream.read())
  }

  function onerror(err) {
    cleanup()
    reject(err)
  }

  function cleanup() {
    stream.removeListener('readable', onreadable)
    stream.removeListener('error', onerror)
    stream.removeListener('end', cleanup)
    // this promise is done, so we lower the maximum number of listeners
    stream.setMaxListeners(stream.getMaxListeners() - 1)
  }
})

This allows you to acknowledge the limit and keep your event handling in control, while allowing node.js to print an error message if an actual leak occurred.

Help write better code!

If you simply .setMaxListener(0), then you may be unknowingly leaking. If you see any code (especially open source) that uses .setMaxListeners(0), make a pull request to fix it! Don't take shortcuts!