読者です 読者をやめる 読者になる 読者になる

Node.jsの例外処理(イベントループによって分断されるコールスタック)

setTimeout()や、process.nextTick()で設定されるコールバック関数の中で例外が発生した場合、アプリケーションにて通常にtry-catchで補足できないのは、Node.jsの中核たるイベントループで遅延スケジュールされた関数のコールスタックが分断されるためです。nodeコマンドに引き渡されたアプリケーションのメインたるJavaScriptのソースを読み込んで実行していくのですが、このラウンドでその後に続くイベントループ中の各ラウンドにコールバックをバインド(後で必要なときに呼び出せるように整理して登録)します。そして続きprocess.nextTick()を実行するラウンド、WEBリクエストなどのI/Oイベントを受け付けて実行するラウンド、Timer系処理のラウンド、とぐるぐる回っていきます。ループはちょうど数字の6のような形になっていてメインは一度だけの実行ながらそこから派生した関数群はずっと高速回転で動き続けます。おかしなコードでループがブロックされない限り。
後のラウンドに整理登録された関数は、メインの実行完了後にも捨てずに残されたグローバル空間で動くので変数やオブジェクトも関数内から触ることができますが、その関数の発火はコールバックの登録コードではなく、イベントループでのスケジューラもしくはEventEmitterがI/Oに反応したところからで、例外発生箇所からさかのぼってたどれるコールスタックのルートは別物になってしまってるのです。

function fireError(flag) {
    if(flag) {
        throw new Error('not callback!'); //ここはコールバックじゃない
    }
    return (function() { throw new Error('this is callback!'); });
}

try {
    setTimeout(fireError(true), 0); //fireErrorは引数を渡して実行している
} catch(e) {
    console.log('catch: ' + e.message);
}

たとえば上記コードはcatchできる。setTimeout()で登録するコールバックではなく、fireError()を関数実行しているのでメインの実行ラウンドで例外が発生しているから。サンプルは奇妙な例ですがexpressとかでは似たようなコードがあります。Domain#bind()も実は似た感じ。
以下は、きちんと例外処理されます。

function fireError(flag) {
    try {
        throw new Error('not callback!');
    } catch(e) {
        console.log('catch: ' + e.message);
    }
}

setTimeout(fireError, 0); //fireErrorはメインで実行せずコールバック登録

コールバックの中でtry-catchすれば、これはちゃんと、しかもシンプルに例外処理ができる。これをちゃんと各所でやればドメインの出番はありません。じゃあ、ドメインの存在意義ってなにか?

...続く