Node.js, part 6: the EventEmitter — and the 'error' event that crashes your process
Part 6from the Node.js series · 24 parts in all
Almost every Node API is an EventEmitter underneath: servers, sockets,
streams, child processes, the process itself. It looks like the simplest thing in the
runtime, and it has one special case that will end your process. Part 6 is the pattern and
its sharp edges.
The pattern
const { EventEmitter } = require('events');
class JobQueue extends EventEmitter {
add(job) {
this.emit('added', job);
runJob(job)
.then(() => this.emit('done', job.id))
.catch((err) => this.emit('failed', job.id, err));
}
}
const queue = new JobQueue();
queue.on('done', (id) => console.log('done', id));
queue.on('failed', (id, err) => console.error('failed', id, err.message));
queue.once('added', () => console.log('first job queued')); // fires once, then detaches
The special case: 'error'
Every other event name with no listener does nothing. 'error' with no listener
throws, and if nothing catches it the process exits with a stack trace
pointing at the emit. That is deliberate — an unhandled error is worse than a
crash — but it means a library that emits 'error' on a bad input can take down a
server whose author never heard of the event.
The rule that follows: if a class of yours can emit 'error', its constructor
attaches a default handler, or its documentation says so loudly.
Listeners are synchronous — and leak
emit calls every listener before returning, in registration order, so a slow
listener blocks the emitter. And an on() inside a request handler adds a
listener per request, which is the classic listener leak:
// Every request adds one more listener to the shared emitter.
app.get('/status', (req, res) => {
metrics.on('tick', () => res.write('.')); // leak
});
// Fix: attach once, outside the handler. Node warns at 11 listeners,
// and emitter.setMaxListeners(n) exists to silence a warning you have
// actually understood.
Two more tools worth having in your head: emit returns
true if anything listened, which is a useful "did anyone care?" check; and
emitter.on('newListener', ...) / 'removeListener' let a class react
to being observed — how a stream can start reading only once someone is listening. Next:
HTTP/2, the first protocol Node shipped as a first-class citizen.