Node Style Woes - Domains and Promises

D

omains have been the red-headed step child of error handling in Node.js It is a library that has been deprecated since v0.12 and has been awaiting a suitable replacement ever since ( we are at v6.5 at the time of writing ). Until one has been implemented by the Node Core team, it is still de-facto way to deal with error propagation. As Node.js supports more and more ES6 features, I have been upgrading my open source projects where it seem appropriate. In my command line tool package, seeli, I was doing some updates and came across some exceptionally odd behavior around ES6 Promises and implicit Domain binding. In a nutshell - It's broke.

Monkey Patch (ˈməNGkē paCH) -v,. --verb

dynamic modifications of a class or module at runtime, motivated by the intent to patch existing third-party code as a workaround to a bug or feature which does not act as desired

Implicit binding is what happens when you require the domain module from node - It does a little monkey patching of the EventEmitter class so that is knows to propagate errors events to a domain instance. All instances of the EventEmitter created after the domain module has been loaded are all considered to be under the single domain all errors events emitted will be forwarded to the domain handlers.

const domain = require('domain').create();
const events = require('events');

domain.on('error', ( err )=>{
    console.error('An error has occurred');
    console.error( err );
    process.exit(1)
});

let e = new events.EventEmitter();
e.emit('error', new Error('I am an Error'));

If you run this little snippet of code, The error is dumped to stderr and the process exits. Perfect, this is what I want, and Seeli has been dealing with errors this way for years with out a problem. However, in keeping up with the Jones' I upgraded the version of inquirer for seeli which introduces the use of Promises. No problem, just pass a then callback, and move the code down a notch - Simple, right? Nope! Everything stops working. Let's modify the above snippet to illustrate

const domain = require('domain').create();
const events = require('events');

domain.on('error', ( err )=>{
    console.error('An error has occurred');
    console.error( err );
    process.exit(1)
});

let e = new events.EventEmitter();
Promise.resolve(true)
       .then(function( result ){
            e.emit('error', new Error('I am an Error'));
       })

Nothing happens. Alright! OK! Hold on a minute - maybe the Promise is doing some magic for use and the catch handler is being called. Let's try that

const domain = require('domain').create();
const events = require('events');

domain.on('error', ( err )=>{
    console.error('An error has occurred');
    console.error( err );
    process.exit(1)
});

let e = new events.EventEmitter();
Promise.resolve(true)
       .then(function( result ){
            e.emit('error', new Error('I am an Error'));
       })
       .catch(function(err){
           console.log( 'catch handler called' );
           e.emit('error', err);
       })

This prints catch handler called and exits. The error still does not propagate to the domain handler.

$ catch handler called

Closer. What if we use the bind function on our domain to wrap the promise callback. This way if anything goes wrong in our original callback, we should still be notified of it

const domain = require('domain').create();
const events = require('events');

domain.on('error', ( err )=>{
    console.error('An error has occurred');
    console.error( err );
    process.exit(1)
});

let e = new events.EventEmitter();
Promise.resolve(true)
       .then(domain.bind(function( result ){
            e.emit('error', new Error('I am an Error'));
       }))

When we run this snippet, you guessed it - Nothing.

Basically, implicit binding just doesn't jive with promises. The v8 engine has what is called a microtask queue which is outside the control of node ( and domains ). If you want your errors to propagate out of the Promise internals, you need to explicitly add emitter instances to the domain in question.

const domain = require('domain').create();
const events = require('events');

domain.on('error', ( err )=>{
    console.error('An error has occurred');
    console.error( err );
    process.exit(1)
});

let e = new events.EventEmitter();
domain.add( e ); // <- explicit binding

Promise.resolve(true)
       .then(function( result ){
            e.emit('error', new Error('I am an Error'));
       })

Now when we run this snippet things look a little bit better

An error has occured
{ Error: I am an Error
    at /home/esatterwhite/dev/js/node-seeli/kill.js:15:29
    at process._tickDomainCallback (internal/process/next_tick.js:129:7)
    at Module.runMain (module.js:577:11)
    at run (bootstrap_node.js:352:7)
    at startup (bootstrap_node.js:144:9)
    at bootstrap_node.js:467:3 }

The error handling story in Node.js is confusing at best and this only complicates the matter even more. I hope the Node Core team comes up with a solid solution to replace domains and solidify the error handling saga soon. Until then, plain old Node-style callbacks are still the most reliable solution.

class es6 promises domain node