Qubyte Codes

Promises and Node.js event emitters don't mix

Published

To many experienced Node developers, the title of this post will seem intuitively obvious. Nevertheless, it's useful to see what unexpected behaviour can occur when the two are used together. Here's an example:

const { EventEmitter } = require('events');
const emitter = new EventEmitter();

Promise.reject(new Error('Oh noes!'))
  .catch(e => emitter.emit('error', e));

If you run this code (tested in Node v7.3 from a script, not REPL), what do you expect to happen? I expected an uncaughtException event, since an event emitter is emitting an error event with no event handler, but that's not what happens at all. Instead you get an UnhandledPromiseRejectionWarning!

This is bad news. If something genuinely nasty has happened, you might want to emit an error like this which is either explicitly handled or causes an uncaughtException event. Uncaught exceptions should lead to the process exiting, and a promise has just stifled it.

So, what happened? Here's a hint:

const { EventEmitter } = require('events');
const emitter = new EventEmitter();

try {
  emitter.emit('error', new Error('Oh noes!'));
} catch (e) {
  console.log('caught');
}

What do you expect to happen here? Again, at first glance I would have expected an uncaughtException event. uncaughtException events can happen when one of two conditions is met. The first is an error is thrown but not caught (thus the name). The second is when an error event is emitted and the emitter has no handler to deal with it.

Did I say there were two conditions? I meant to say one! The second condition is really the first in disguise. When an emitter lacks an error handler and it emits an error event, the default error handler takes control and throws the error. Since event handlers are called synchronously upon event emissions, this:

const { EventEmitter } = require('events');
const emitter = new EventEmitter();

emitter.emit('error', new Error('Oh noes!'));

is equivalent to

throw new Error('Oh noes!');

So, the very first snippet is equivalent to:

Promise.reject(new Error('Oh noes!'))
  .catch(e => {
    throw e;
  });

The promise chain captures the thrown error and wraps it as a rejected promise, which leads to an UnhandledPromiseRejectionWarning since a second .catch is needed in the chain to handle it.

If you want to ensure that unhandled error events lead to uncaught exceptions which aren't captured by a catch or a promise chain, then you can wrap the emission in a setTimeout or a setImmediate.