Understanding node.js’s possible eventemitter leak error message
Originally published on April 25, 2015.
In node.js and io.js, you’ll frequently 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 when they are no longer needed. 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
const 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((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 never 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!
Originally published at www.jongleberry.com.