Node.jsの例外処理(ドメインの存在意義)

Node.jsはエコシステムが発達していて、コアライブラリだけでなく、サードパーティがたくさん有用な拡張モジュールを提供してくれています。また、自前で書くコードも一つのJSファイルだけで書くのではなく、見通しも良いように、再利用可能なように、機能単位毎にモジュールにまとめて作って行くべきでしょう。その際、ライブラリ的なモジュールでは具体的に実行時例外の処理をすべきではないのが一般的だと思います。例外処理をおこなって例外握りつぶしちゃうと問題が内部で起きている事をアプリケーションレベルできちんと把握できなくなっちゃう。
Node.jsのライブラリ作法としては、例外をそのままthrowするのとは別に、下記の書き方があります(以下、冗長なのでモジュール分割しないで書いてますが...)。

var events = require('events');
var emitter = new events.EventEmitter();
emitter.on('error', function(e) {
    console.log('emitter handled: ' + e.message);
});
emitter.fireError = function() {
    this.emit('error', new Error('Emitter error.'));
}

emitter.fireError(); emitter.fireError(); emitter.fireError();

EventEmitterをプロトタイプ継承したり、もしくはEventEmitterを内部に持ち処理を委譲しているような場合、EventEmitter#emit('error', Error)をもって例外を通知する仕組みが合理的です。try-catch文はtryブロックに続けてcatchブロックを文法的制約上tryとcatchを一対かつ一箇所で書かないといけないのですが、EventEmitterを利用すると、別途に用意したイベントハンドラにて集約処理ができるので、保護したいブロック多数に対して例外処理ブロックを少数かつ違う場所で書けます。上記例では末尾にてfireError()を立て続けに何度も呼び出していますが、これは他所で五月雨に呼び出してもOKなのです。

var d = require('domain').create();
d.on('error', function(e) {
    console.log('domain handled: ' + e.message);
});

var events = require('events');
var emitter1, emitter2;

d.run(function() {
    emitter1 = new events.EventEmitter(); //暗黙のドメイン登録
    emitter1.fireError = function() {
        this.emit('error', new Error('Emitter1 error.'));
    }
});

emitter2 = new events.EventEmitter();
emitter2.fireError = function() {
    this.emit('error', new Error('Emitter2 error.'));
}
d.add(emitter2); //明示なドメイン登録

emitter1.fireError(); emitter1.fireError(); emitter1.fireError();
emitter2.fireError(); emitter2.fireError(); emitter2.fireError();

ドメイン使って書くとこんな感じ。EventEmitterにて書いてた例外処理ブロックをさらにドメインに集約することができますので、複数のEventEmitterを一つのドメインで処理できます。EventEmitterのドメインへの登録は、コンストラクタがDomain#bind()/#run()/#intercept()の引数に渡される関数内に書かれているときには自動的に参加します。EventEmitterコンストラクタで参加ロジックが記述されているので、EventEmitterをプロトタイプ継承させる場合は、以下のような処理が絶対必要。

var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;
function ChildEmitter() {
    EventEmitter.call(this); //この実行でドメインへの自動参加を行うことになる
}
inherits(ChildEmitter, EventEmitter); //これはNode.jsの定番な書き方

emitter2の方は、Domain#add()でドメイン参加させています。アプリケーションコードでは無いところでコンストラクタの呼び出しがあった場合に用いられます。たとえば、コレ。

// appはexpressのappだとして...
app.use(function(req, res, next) {
    var d = domain.create();
    d.on('error', function() { /*具体的な例外処理*/ });
    d.add(req);
    d.add(res);
    d.run(next);
});

express〜connectのミドルウェアの中でドメイン使うとすると、Domain#run()する前に引数で渡されてくるreqおよびresは作られちゃっている。このreqおよびresはEventEmitterの果ての子孫なのでこういう感じで。ミドルウェアの中でonハンドラ書いているのは、reqとresが直接見えるためで、たぶんこうしていいのだと思うけど、よりよいやり方があるかもしれない。
このほか、先にNode.jsのコードを見てきたように、コールバックやTimer系関数はいちいちドメインに対応しているのできちんとDomain#run()を使ってあげるとイベントループによるコールスタックの断絶にも関わらずきちんと例外補足してくれます。

まとめ:

  • Domain#run()/bind()で保護する。try-catchと異なりprocess.nextTick()やTimer系でもOK。
  • Domain#run()/bind()で保護できないときにはDomain#add()
  • Domain#on('error')で例外処理を書く
  • モジュールではEventEmitter#emit('error')を活用してみる。throwままでも支障無いけど
  • Domain#enter()/exit()はローレベルファンクションだとおもうのでアプリ開発では使わない