You could have invented domains

(and I needed to)

Who am I?

  • working with Node.js for a year and a half
  • building Node.js instrumentation for New Relic
  • generally a glutton for punishment

What's in this talk?

  • Error handling in JavaScript
  • ...with complications provided by Node.js
  • the how and why of domains
  • domains as generic per-request state

What's my angle?

I needed a good way of tying information about a request-response cycle to a specific transaction so I could create useful transaction traces for New Relic.

Basic error handling in JavaScript

Exceptions are part of JavaScript, and will do basically what you expect:


var object;
// will throw with a useful stacktrace
console.log(object.field);
        

Basic error handling in JavaScript

Easily caught with try / catch:


var object;
try {
  console.log(object.field);
}
catch (error) {
  console.error("got an error: %s", error);
}
        

But Node.js is asynchronous

What happens when you run this?


var object;
try {
  process.nextTick(function () {
    console.log(object.field);
  });
}
catch (error) {
  console.error("got an error: %s", error);
}
      

(hint: nothing good)

A more typical example


var http = require('http');

var server = http.createServer(function (req, res) {
  res.send("<html><head><title>page</title>" +
           "<body>yo dawg I heard u like HTML</body></html>");
});

server.listen(8080);
        

…especially in that it's not immediately obvious what's wrong.

the uncaught exception handler


process.on('uncaughtException', function (error) {
  console.error("got an error, no idea where it came from: %s", error);
});

var http = require('http');

var server = http.createServer(function (req, res) {
  res.send("<html><head><title>page</title>" +
           "<body>yo dawg I heard u like HTML</body></html>");
});

server.listen(8080);
        

…well, at least your entire app no longer crashes. (Note: the uncaught exception handler actually lives inside V8.)

Node.js conventions

  • only use exceptions for "fail-fast" (internal) errors
  • back in the old days, callback / errorback convention was popular
  • now conventionalized to first parameter of callback being reserved for errors

Node.js conventions


var request = require('request');

request('http://example.cob/', function (error, response, body) {
  if (error) return console.error("error fetching data: %s", error);

  console.error("body: %s", body);
});
        

domains

domain facts

  • simple API for adding context to actions
  • incredibly poorly named
  • little bits of code throughout Node's source to enable more transparent error handling
  • Only available in Node.js 0.8+

So before we had this:


var http = require('http');

var server = http.createServer(function (req, res) {
  res.send("<html><head><title>page</title>" +
           "<body>yo dawg I heard u like HTML</body></html>");
});

server.listen(8080);
      

and now we have this:


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

var server = http.createServer(function (req, res) {
  var handlerDomain = domain.create();
  handlerDomain.on('error', function (error) {
    console.error('Oh! Request handler for URL %s got error: %s',
                  req.url, error);
    res.statusCode = 500;
    res.end();
  });
  handlerDomain.run(function () {
    res.send("<html><head><title>page</title>" +
             "<body>yo dawg I heard u like HTML</body></html>");
  });
});
server.listen(8080);
      

Points of interest:

  1. each invocation gets its own error handling
  2. can scope cleanup to the specific action
  3. no longer need to rely on stack traces

If you have a callback following the conventions:


var request = require('request');

request('http://example.cob/', function (error, response, body) {
  if (error) {
    return console.error("error fetching data: %s", error);
  }

  console.error("body: %s", body);
});
      

You can now use domain.intercept():


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

var rd = domain.create();
rd.on('error', function (error) {
  console.log("error fetching data: %s", error);
});

request('http://example.cob/',
        rd.intercept(function (response, body) {
  console.error("body: %s", body);
}));
      

What about Event Emitters?

  • accounts for most of Isaac's work on domains
  • covers most everything
  • …except for a few caveats, covered later

This…


var object;
var handle = setInterval(function () {
  console.error("value of object.field: %s", object.field);
}, 500);
      

…becomes this:


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

var d = domain.create();
d.on('error', function (error) {
  console.error("Got error: %s", error);
  console.error("error.domain: %s", util.inspect(error.domain));
});

var object;
var handle = setInterval(function () {
  console.error("value of object.field: %s", object.field);
}, 500);

d.add(handle);
      

…or this:


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

var d = domain.create();
d.on('error', function (error) {
  console.error("Got error: %s", error);
  console.error("error.domain: %s", util.inspect(error.domain));
});

var object;
var handle = setInterval(d.bind(function () {
  console.error("value of object.field: %s", object.field);
}), 500);
      

Under the hood

How does all this work?

  • Node.js is single-threaded
  • - no thread-local storage
  • + many fewer race conditions

This means we can create a shared request-local state.

The domain is a simple object, and when it's active, it's stored in two places:


// what you should use if you're writing domain-aware applications
domain.active;

// the one library authors will actually want to use most often
process.domain;
      

The various ways to use domains all boil down to the same thing


d.run(function () { /* risky code goes here*/ });
// ...is the same as...
d.bind(function() {})(); // <-- invoke immediatly
// ...is the same as
(function () { d.enter(); /* risky code */ d.exit(); }())
      

domain.enter() and domain.exit() are the key functions.

Remember our friend the uncaught exception handler?

The domain system installs an uncaughtException handler to ensure that it catches all events. In certain rare circumstances, this doesn't work (which is probably a bug).

Caveats

This works great, except when there are EventEmitters created before the domains were created (e.g. database connections with connection pools). Either use domain.bind() or bug the package authors to update their code.

Advanced topics

  • What I needed for New Relic
  • Abusing domains for global state
  • What library writers need to do

Questions