非同期APIと例外処理(node.js の domain について)
2012.10.20
node.js のような非同期APIを使ったプログラミングに拒絶反応を示すエンジニアが多い理由の一つが、非同期APIと例外処理の相性の悪さだ。
Javascript の場合、例外処理はこんな感じに記述する。
function f(i) {
try {
throw new Error('an error #'+ i);
} catch(e) {
console.log('Error caught:', e.message);
}
}
ところが、これに非同期APIが絡むと、とたんに分かりにくくなる。例えば下の例。
function f(i) {
try {
setTimeout(function() {
throw new Error('an error #'+ i);
}, 1000);
} catch(e) {
console.log('Error caught:', e.message);
}
}
setTimeout に渡された無名関数は try ブロックから抜けた後に非同期に実行されるため、例外をキャッチできないのだ。
確かにこれは、直感的なプログラミングとは言えず、こんな部分が拒絶反応を引き起こす人が多いのも納得できる。
ちゃんと例外処理をするためには、
function f(i) {
setTimeout(function() {
try {
throw new Error('an error #'+ i);
} catch(e) {
errorHandler(e);
}
}, 1000);
function errorHandler(e) {
console.log('Error caught:', e.message);
};
}
のように例外を発生する可能性のある部分を個別に try ブロックで囲み、例外をキャッチしたことをイベントハンドラーやイベントエミッター(もしくは、コールバック関数のパラメータ)を通して上位のモジュールに明示的に知らせる必要がある。
しかし、このやり方は、手間がかかるし(すなわち、ケアレスミスによるバグを発生しやすい)、サードパーティのライブラリを使う場合などには応用できない。
この欠点を補うために、node.js に v0.8 から導入されたのが、domain である。domain は非同期に実行されるコールバック関数を一つのコンテキストにまとめて、例外処理をする仕組みで、以下のように使う。
function f(i) {
var d = require('domain').create();
d.run(function() {
setTimeout(function() {
throw new Error('an error #'+ i);
}, 1000);
});
d.on('error', function(e) {
console.log('Error caught:', e.message);
});
}
非同期に呼び出された関数内で発生した例外を try ブロックでキャッチできないという JavaScript の欠点を補うために導入された、「非同期版 try ブロック」のようなものだと考えておけば良いだろう。
domain は現時点(v0.8.12)ではまだ "Experimental" な機能なので、今後 API が変更される可能性も十分にあるので注意が必要だ。
ちなみに、domain の実装が正しくされているかどうか確かめる、簡単なストレステストを書いてみたので下に貼付けておく。
for (var i=0; i<10; i++) {
(function(i) {
setTimeout(function() { f(i) }, 100);
})(i);
}
function f(i) {
var d = require('domain').create();
d.run(function() {
setTimeout(function() {
throw new Error('an error #'+ i);
}, Math.random() * 1000);
});
d.on('error', function(e) {
console.log('Error caught:', e.message, ' in context ', i);
});
}
Comments