Trust is earned, not given

A different perspective

2019-05-21 · Projects

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.