Node.js domains

your friends and neighbors

Forrest L Norvell & Domenic Denicola, nodeconf 2013

who

  • Forrest works on Node instrumentation for New Relic.
  • Domenic works for lab49, maintains many JavaScript libraries, contributes to the evolution of JavaScript, and co-maintains the Promises/A+ standard.

domains: a very brief overview

  • try-catch doesn't work with async and uncaughtException is gross
  • domains added in 0.8 to simplify async error-handling
  • adds a standard mechanism for handling all the common error use cases
  • incurs a small performance penalty, but that's error-handling

getting started with domains


var domain = require("domain");

var d = domain.create();
d.on("error", function (error) {
  console.error("Well, this sucks: %s", error.message);
  process.exit(1);
});
d.run(function () {
  // do error-prone stuff here
});
        

Node.js "hello world" with domains


var http = require('http'), domain = require('domain');

http.createServer(function (req, res) {
  var d = domain.create();
  d.on("error", function (error) {
    res.writeHead(500, {'Content-Type': 'text/plain'});
    res.end('Couldn\'t fulfill request because of an error: ' +
            error.message);
  });
  d.run(function () {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
  });
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
        

domain.add

domain.add(emitter) binds an EventEmitter to a domain.

  • important: Don't add streams or emitters that will be reused for multiple requests to a domain.
  • Best to think of binding to a domain as immutable.

request and response added to domain


var http = require('http'), domain = require('domain');

http.createServer(function (req, res) {
  var d = domain.create();
  d.on("error", function (error) { /* ... */ });

  d.add(req);
  d.add(res);

  d.run(function () { /* ... */ });
}).listen(1337, '127.0.0.1');
        

let's handle some errors, part 1

a basic web server

Express, middleware, & error handling

  • Express is built on Connect, which composes middleware (functions) into continuation chains
  • each middleware function is evaluated in a try-catch clause, and errors are propagated via the continuations
  • to recover these errors, you need to use an error-handling middleware (e.g. express.errorHandler)
  • two ways to combine domains with Connect error handlers:

Express & domains 1

wrap every request in a domain, use the default error handler


app.use(function (req, res, next) {
  var d = domain.create();
  d.on("error", next);
  d.run(next);
});
// all the other middlewares in the stack
app.use(express.errorHandler());
        

One nice thing is that the built-in error handler respects the Accept header coming from the user agent.

Express & domains 2

create domains explicitly in routes, trap via error handler


var defaultHandler = express.errorHandler();
app.use(function (error, req, res, next) {
  if (domain.active) {
    domain.active.emit("error", error);
  } else {
    defaultHandler(error, req, res, next);
  }
});
app.get("/crashy", function (req, res) {
  var d = domain.create();
  d.on("error", function (error) {
    res.send(500, {"error": error.message});
  });
  d.run(function () { throw new Error("oops"); });
});
        

let's handle some errors, part 2

Express server with some crippling issues

binding functions into domains

  • domain.bind(fn) ensures that any errors inside fn are trapped by the domain.
  • domain.intercept(fn) is the same, but takes a conventional Node callback and traps the error parameter in the domain. The parameter must be an instance of Error.

var d = domain.create();
// ...
fs.readFile("filename.txt", function (error, data) {
  if (error) return d.emit("error", error);
  // do stuff with contents
});
        

becomes


var d = domain.create();
// ...
fs.readFile("filename.txt", d.intercept(function (data) {
  // do stuff with contents
}));
        

connection pools are weird

  • a single EventEmitter used across many requests
  • not all connection-pooling libraries are updated to use domains (well, OK, most haven't been)
  • use domain.bind(callback) (or domain.intercept(callback)) to trap errors in your code.
  • can also run the connection pool inside its own domain using domain.run(), but you'll still want to explicitly bind your application callbacks to the lexically closest domain

var pool;
var poolD = domain.create(); poolD.run(function () {
  pool = new mydb.Pool();
  poolD.on("error", function (error) {
    console.error("Connection pool exploded with '%s'. Trying clean shutdown.", error.message);
    pool.flush();
    pool.close(function () { process.exit(1); });
  });
});
var client = require("./mydb/client.js")(pool);
function processUsers(error, users) { /* do stuff */ }
function getUsers() {
  var d = domain.create();
  d.on("error", function (error) { /* custom error-handling */ });
  client.getUsers(d.bind(processUsers));
}
        

let's handle some errors, part 3

a proof of work server with a connection pool

informative shutdown vs On Error Resume

  • Node uses side effects, so more often than not, errors leave the process in an undefined state.
  • Better safe than sorry: capture error for user, shut down cleanly.
  • If actions are side-effect free, you can capture the error and move on.
  • How often do you know actions are free of effects? Do you want to make Isaac sad?

cast off your mental chains

use domains with confidence

  • Between domain.run(), domain.bind(), domain.add(), and domain.intercept() you can handle all three of the common mechanisms for signaling errors in node (callback convention, error events, and try/catch).
  • It's often easier just to throw inside code that will be run in a domain and let Node handle it.
  • Also maybe faster than try/catch.

extras & bonus stuff

more technical aspects of domains

domains from the bottom up

  • domain.enter() and domain.exit() are the primitives that set up and tear down domains.
  • domain.exit() gets called both when errors are trapped and when domains are exited normally.

domains from the bottom up

  • domain.enter() and domain.exit() are the primitives
  • Conceptually, domain.bind() is a wrapper around Function.bind(), domain.enter(), and domain.exit():

domains from the bottom up

    
    Domain.prototype.bind = function (callback) {
      return function () {
        this.enter();
        callback();
        this.exit();
      }.bind(this);
    };
    
    Domain.prototype.intercept = function (callback) {
      return function (error) {
        if (error) return this.emit("error", error);
        this.enter();
        callback.apply(null, Array.prototype.slice.apply(arguments, 1));
        this.exit();
      }.bind(this);
    }
              

domains from the bottom up

  • In turn, domain.run() is just an immediately invoked version of domain.bind().

nesting domains

  • Node maintains a domain stack
  • domain.enter() pushes a domain on the stack, domain.exit() pops that domain and all the domains above it on the stack
  • this is analogous to how the nonlocal exit of a throw pops multiple stack frames in traditional exception handling
  • it does limit the usefulness of domains for things other than error handling

finding the active domain

  • in two places: process.domain and domain.active
  • can feature-test for process.domain in code where domains are optional or can be used opportunistically
  • for code built explicitly to be used with domains, use domain.active
  • the above are just stabs at conventionalizing usage – domains are still experimental and this isn't frozen yet

bonus round: convince Domenic to talk about domains and promises