ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

node.js Domain 時代のエラー処理のコーディングパターン

id:kazuhooku さんの記事 node.js におけるエラー処理のコーディングパターン (もしくは非同期 JavaScript における例外処理。
ナイスです! なんと素晴らしいタイミングでのブログでしょうか!

「東京Node学園 5時限目」で id:koichik さんのプレゼンで node-v0.7.8 から isaacs 版 Domain が導入されるという発表がありましたが、予定通り昨日 Domain 機能付きの node-v0.7.8 がリリースされました。

しかもDomain のドキュメント付きです。 http://nodejs.org/docs/v0.7.8/api/domain.html

ちょうど id:kazuhooku さんの記事の例は node.js の新機能 Domain を教科書通りに適応するとどうなるのか紹介するのにぴったりのお題なので
Node.js v0.8 の新機能 Domain の使い方
について書かせていただきます。

Node.jsの新機能 Domain を使う

まずドメインを利用してエラー処理するには

  • domain モジュールの require()
  • domain オブジェクトの生成
  • domain で受けるエラーリスナーの登録
  • domain とエラーハンドリングとの結び付け
    • eventEmitter の場合は Domain.add
    • 関数の場合は Domain.bind

といった段取りが必要です。
id:kazuhooku さんの記事にあるコードを、Domain 使ったらどのようなコーディングパターンが考えられるかいくつ例示してみたいと思います。。(注:動作には node-v0.7.8 以降が必要です。)

今回3つのパターンで考えました。

  • パターン1: コールバック関数にドメインを結びつけてエラーハンドリングを行う
  • パターン2: 関数呼び出し時にドメインを結びつけてエラーハンドリングを行う
  • パターン3: 関数定義時にドメインを結びつけてエラーハンドリングを行う

ではサンプルコードとともにドメインを使ったコーディングパターンを例示していきます。

パターン1: コールバック関数にドメインを結びつける方法

まずはエラーオブジェクトを含むコールバックでドメインとエラーを結びつける場合です。

// domain_sample1.js
var domain = require('domain');
var fs = require('fs');
var d = domain.create();

function countChars(filename, callback) {
  // コールバックに直接ドメインをバインド
  fs.readFile(filename, 'utf-8', d.bind(function(err, data) {
    if (err) throw err;
    callback(data.length);
  }));
}

function main(args) {
  countChars(args[0], function(length) {
    console.log(length);
  });
}

d.on('error', function(err) {
  console.log(err.message);
});

main(process.argv.slice(2))
unixjp:~/tmp/github/node> ./node domain_sample1.js hoge
ENOENT, open 'hoge'

はい、きれいなコードになりましたね。

パターン2: 関数呼び出しに時にドメインを結びつける方法

関数を呼び出すときにバインドしたい場合は、

// domain_sample2.js
var fs = require('fs');
var domain = require('domain');
var d = domain.create();

function countChars(filename, callback) {
  fs.readFile(filename, 'utf-8', function(err, data) {
    if (err) throw err;
    callback(data.length);
  });
}

function main(args) {
  // 関数を呼び出す時にドメインをバインドする。
  (d.bind(countChars))(args[0], function(length) {
    console.log(length);
  });
}

d.on('error', function(err) {
  console.log(err.message);
});

main(process.argv.slice(2));

な感じです。

パターン3: 関数定義にドメインを結びつける方法

関数定義直後にバインドするなら、

// domain_sample3.js
var fs = require('fs');
var domain = require('domain');
var d = domain.create();

function countChars(filename, callback) {
  fs.readFile(filename, 'utf-8', function(err, data) {
    if (err) throw err;
    callback(data.length);
  });
}
// 関数定義直後にドメインにバインド
countChars = d.bind(countChars);

function main(args) {
  countChars(args[0], function(length) {
    console.log(length);
  });
}

d.on('error', function(err) {
  console.log(err.message);
});

main(process.argv.slice(2));

といったようになります。

まとめ

他には event.EventEmitter のエラーと結び付けたい場合は Domain.bind() ではなく Domain.add() すればドメイン内のエラー処理で扱えます。
このように Domain の機能を使うとこれまでと違った形でエラーハンドリングをする可能性が広がり、従来より可読性の高く・簡潔なコードが書けるようになるでしょう。

追記: Domain.intercept() を使う

id:koichik さんからのコメントにあるよう domain.bind(cb, true) だと throw しなくてもエラーをひっかけてくれます。(呼ばれる関数の第一引数が Error インスタンスであることが条件ですけど) これをまとめて domain.intercept() という関数になってます。
ということで domain.intercept() を使うともっとすっきりに書けます。

// domain_sample4.js
var domain = require('domain');
var fs = require('fs');
var d = domain.create();

function countChars(filename, callback) {
  fs.readFile(filename, 'utf-8', d.intercept(function(err, data) {
    callback(data.length);
  }));
}

function main(args) {
  countChars(args[0], function(length) {
    console.log(length);
  });
}

d.on('error', function(err) {
  console.log(err.message);
});

main(process.argv.slice(2));
unixjp:~/tmp/github/node> ./node domain_sample4.js hoge
ENOENT, open 'hoge'

ありがとうございます。 > id:koichik さん。

追記の追記: Domain.run() を使う。

またまた id:koichik さんから twitter 経由で Domain.run() の使い方を教えていただきました。(実はマニュアルには記載されていませんけど)→ id:koichik さんからのコメントで public API としてドキュメント化されました。https://github.com/joyent/node/commit/c0a9985da7a7d3f6ebc51805d6955d92b1bd6e78
これを使うと上記パターン2(関数呼び出しに時にドメインを結びつける方法)のコードで (d.bind(fn))(args) と書いていたところが d.run(function() {fn(args)} と書けて、もっとすっきりします。

// domain_sample5.js
var fs = require('fs');
var domain = require('domain');
var d = domain.create();

function countChars(filename, callback) {
  fs.readFile(filename, 'utf-8', function(err, data) {
    if (err) throw err;
    callback(data.length);
  });
}

function main(args) {
  // 関数を呼び出す時にドメインをバインドする。
  d.run(function() {
    countChars(args[0], function(length) {
      console.log(length);
    });
  });
}

d.on('error', function(err) {
  console.log(err.message);
});

main(process.argv.slice(2));