Node Tuts

Detach script window ⇗

You can navigate the video and the script by using the ← and → cursor keys.

Domains

In this episode we are going to cover the usage of domains and how that can help to make your Node.js application server resilient and manageable on the event of errors.

Life Before Domains

Before domains were introduced in Node v0.8, the life of a Node.js server was harder. Whenever an error was thrown, the Node process would log the error and exit.

01_server_throws.js:

var server = require('http').createServer();
server.on('request', function(req, res) {
  a.abc();
  res.end('Thank you for using our service!');
});
server.listen(8080);

In this example we are forcing an error to be thrown when processing any request, but this type of error could happen occasionally, bringin your entire server down, and with it all the clients that were connected.

To prevent Node.js from exiting, we could listen for the uncaughtException event on the global process object:

02_uncaught_exception_handler.js:

process.on('uncaughtException', function(err) {
  console.log('uncaught exception here', err);
});

var server = require('http').createServer();
server.on('request', function(req, res) {
  a.abc();
  res.end('Thank you for using our service!');
});
server.listen(8080);

This prevents your Node process from going down, but you leave it with a resource leak, which in this case is an open connection to the client on an unresponded request.

The most responsibla thing to do in this case is to log the error and exit the process.

Event Emitters and Error Handling

All events types are equal to an event emitter with one exception: the error event, which, if not handled, has a similar effect as an uncaught exception.

Let's see what happens when an event emitter emits an error event.

var EventEmitter = require('events').EventEmitter;

var server = require('http').createServer();
server.on('request', function(req, res) {
  var ee = new EventEmitter();
  ee.emit('example', 1, 2, 3);
  ee.emit('error', new Error('something terrible has happened'));
  res.end('Thank you for using our service!');
});
server.listen(8080);

Here we instantiated a new event emitter just to mimick what happens, for instance, when you do an HTTP request or any other kind of operation that might fail on an event emitter.

Here you can see that emitting the example event does not pose a problem, but the error event originates an uncaughtException that terminates the process.

Without using domains, the best way to handle this is to listen to the error event, which prevents the uncaughtException from being thrown.

04_event_emitter_handle_error.js:

var EventEmitter = require('events').EventEmitter;

var server = require('http').createServer();
server.on('request', function(req, res) {
  var ee = new EventEmitter();

  ee.on('error', function(err) {
    res.writeHead(500);
    res.end(err.message);
  });

  ee.emit('example', 1, 2, 3);
  ee.emit('error', new Error('something terrible has happened'));
  res.end('Thank you for using our service!');
});
server.listen(8080);

Enter domains

Domains, which were introduced in Node v0.8, come to solve these types of problems of error handling. Using a domain you can channel all the errors happening in the context of that domain.

Domains propagate across event emitters. When an event emitter is constructed, it inherits the current domain, and when an event emitter emits, that domain is entered.

Here is how you can use domains in our previous server:

05_domains.js:

var EventEmitter = require('events').EventEmitter;
var domain = require('domain');

var server = require('http').createServer();
server.on('request', function(req, res) {

  var d = domain.create();

  d.once('error', function(err) {
    res.writeHead(500);
    res.end(err.message);
  });

  d.run(function() {
    var ee = new EventEmitter();

    ee.emit('example', 1, 2, 3);
    ee.emit('error', new Error('something terrible has happened'));
    res.end('Thank you for using our service!');    
  });

});
server.listen(8080);

As you can see, instead of firing an uncaughtException, by wrapping the execution inside a domain.run call, the event emitter got automatically attached to the active domain.

Besides binding event emitters, it can also catch errors thrown into the run context:

06_domains_throw.js:

var EventEmitter = require('events').EventEmitter;
var domain = require('domain');

var a;

var server = require('http').createServer();
server.on('request', function(req, res) {

  var d = domain.create();

  d.on('error', function(err) {
    res.writeHead(500);
    res.end(err.message);
  });

  d.run(function() {
    a.abc();
    var ee = new EventEmitter();

    ee.emit('example', 1, 2, 3);
    ee.emit('error', new Error('something terrible has happened'));
    res.end('Thank you for using our service!');    
  });

});
server.listen(8080);

Here you can see that the call to a.abc() throws an error, but that error does not stop the process and is conducted to the domain.

This also works if the error is thrown asynchronously:

07_domains_throw_async.js:

var EventEmitter = require('events').EventEmitter;
var domain = require('domain');

var a;

var server = require('http').createServer();
server.on('request', function(req, res) {

  var d = domain.create();

  d.on('error', function(err) {
    res.writeHead(500);
    res.end(err.message);
  });

  d.run(function() {
    setTimeout(function() {
      a.abc();
    }, 500);
  });

});
server.listen(8080);

Explicitly Adding Event Emitters

We saw how event emitters are explicitely added to a domain if they're run in the context pf a domain. But if you don't have a domain setup when the event emitter is created, you can explicitely add event emitters like this:

08_domains_add.js:

var EventEmitter = require('events').EventEmitter;
var domain = require('domain');

var a;

var server = require('http').createServer();
server.on('request', function(req, res) {

  var d = domain.create();

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

  var replied = false;

  d.on('error', function(err) {
    if (! replied) {
      replied = true;
      res.writeHead(500);
      res.end(err.message);      
    }
  });

  d.run(function() {
    setTimeout(function() {
      res.emit('error', new Error('halp!'));
    }, 500);
  });

});
server.listen(8080);

Here we added the request and response objects, which are also event emitters. In the event of an error, we now check if we already tried replying. To prevent If we did, we don't try again, since the error was probably generated by trying to reply.

Intercepting callbacks

Besides event emitters, I/O is also processed in the context of callbacks. Domains also support callbacks. For instance, if you're reading a file:

09_domains_intercept.js:

var EventEmitter = require('events').EventEmitter;
var domain = require('domain');
var fs = require('fs');

var a;

var server = require('http').createServer();
server.on('request', function(req, res) {

  var d = domain.create();

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

  var replied = false;

  d.on('error', function(err) {
    if (! replied) {
      replied = true;
      res.writeHead(500);
      res.end(err.message);      
    }
  });

  d.run(function() {
    fs.readFile(__filename, d.intercept(function(contents) {
      res.end(contents);
    }));
  });

});
server.listen(8080);