例外処理

ここでは node.js に特有な例外処理について確認します.

node.js は 1スレッド

node.js はイベントループという仕組みによって実行されています.

Event1 -> Event2 -> Event3 -> ... と実行されていく中で、途中のイベントが例外を処理し損ねた場合はプロセス全体が停止します.

次のスクリプトは必ず5秒後にプロセスが停止します.

function loop(a){
   console.log(a);
   setTimeout(loop, 1000, a + 1);
}

setTimeout(function(){
   console.log('start');
   loop(0);
}, 0);

setTimeout(function(){
   throw "Stop"
}, 5000);

イベントループの実行モデルを展開すると次のようになるでしょう.

console.log('start')
loop(0)
loop(1)
loop(2)
loop(3)
loop(4)
throw "Stop"
loop(5)  // 実行されない
loop(6)  // 実行されない
...

setTimeout がスレッドを生成して実行させる実装であればこうはなりません。 throw “Stop” のスレッドだけが停止し,メインスレッドはループを続けるでしょう.しかし,node.js の世界は違います.どこか一つでもプログラムに未対応の例外が存在し,それが投げられてしまうとプロセス全体が消滅してしまいます.

このことから node.js を使う開発者は例外を正しく処理することを心がけなければ成りません.

EventEmitter の特殊な例外

例外が起こるケースはいくつかありますが,初心者が多く遭遇するケースはerrorイベントの処理漏れでしょう.

次のプログラムを実行してみてください.

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

function loop(){
   var e = new EventEmitter();
   function _loop(a){
      e.emit('loop', a);
      if( (a % 10) === 0 ){
         e.emit('error', new Error(a));
      }
      setTimeout(_loop, 1000, a + 1);
   }
   // start;
   setTimeout(_loop, 0, 1);
   return e;
}

var ev1 = loop();
ev1.on('loop', function(i){
   console.log(i);
});

var ev2 = loop();
ev2.on('loop', function(i){
   console.log(i);
});
ev2.on('error', function(){});

このスクリプトは10数えると次のような例外で停止します.

events.js:14
        throw new Error("Uncaught, unspecified 'error' event.");
              ^
Error: Uncaught, unspecified 'error' event.
    at EventEmitter.emit (events.js:14:15)

EventEmitter は emit(‘error’) に対して敏感です.このエラーイベントを処理しない場合は例外を投げるような実装になっています.

次のようなコードを追加すれば大丈夫でしょう.もちろん握りつぶしはよくないので,実際のコードではきちんと例外処理を実装してください.

ev1.on('error', function(err){})
ev2.on('error', function(err){})

本当にすべての例外を捕まえられるの?

こういう言い方をすると怒られるかもしれませんが,「例外」というのは予期しないときに起こるもので,予期していないのですから対策は漏れがちです.すべての人が完璧なプログラムを書けるのであれば,プログラムを動かす前にすべての例外に対する処理コードを記述しておくのが理想でしょうが,現実はそう甘くはありません。

しかし,たった1つの例外でプロセス全体が死んでしまうのは困ります.

node.js では例外が発生してもプロセスが落ちないような保険をかけておくことができます.process の uncaughtException イベントです.

var EventEmitter = require('events').EventEmitter;
process.on('uncaughtException', function(err){
   console.log(err.stack);
});

function loop(){
   var e = new EventEmitter();
   function _loop(a){
      e.emit('loop', a);
      if( (a % 5) === 0 ){
         e.emit('error', a);
      }
      setTimeout(_loop, 1000, a + 1);
   }
   // start;
   setTimeout(_loop, 0, 1);
   return e;
}

// ループ1
var ev1 = loop();
ev1.on('loop', function(i){
   console.log(i);
});

// ループ2
var ev2 = loop();
ev2.on('loop', function(i){
   console.log(i);
});
ev2.on('error', function(){});

このコードは実行するとプロセスが停止しません.最初に実行している loop() は errorイベントのリスナが定義されていないため,ループは途中で終了してしまいます.しかし,uncaughtException イベントリスナにより,スタックトレースを出力しますがプロセスが終了することはありません.

次のような実行モデルになります.

loop(1)
loop(1)
loop(2)
loop(2)
loop(3)
loop(3)
loop(4)
loop(4)
loop(5)
loop(5)
loop(6)
loop(6)
loop(7)
loop(7)
loop(8)
loop(8)
loop(9)
loop(9)
throw new Error('Uncaught, unspecified 'error' event.");
loop(10)
loop(11)
loop(12)
...

loop() 関数自体がerrorイベントを適切に処理すればイベントループを生成し続けるので,プロセスはずっと生きたままになります.

尚,uncaughtException イベントは、例外発生元を救出するための機構ではないことに注意してください.あくまで,サーバープログラムのような常駐型プロセス内で,特定の例外によりプロセス全体が死ぬのを防ぐための機構です.

イベントを発火する側も注意を.

次のような,リスナ側が例外を発生させるケースでは,当然ループは停止します. EventEmitter.emit() は単に登録されたリスナ関数をcallメソッドを使って呼び出すだけです.

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

var ev = new EventEmitter();
function loop(a){
   ev.emit('loop', a);
   if( a % 10 == 0 ){
      ev.emit('loop2', a);
   }
   setTimeout(loop, 1000, a + 1)
}

loop(1);
ev.on('loop', function(i){
   console.log(i);
});
ev.on('loop2', function(i){
   throw new Error('loop2');
});

この例だと10秒後に,リスナ側が原因でループが終了してしまいます.この例では問題にはなりませんが,イベント発火側がファイルディスクリプタなどの有限リソースを使っているときに注意しなければなりません.次のように finally を使って有限リソースの解放を行う必要があります.

if( a % 10 == 0 ){
   try{
      ev.emit('loop2', a);
   }finally{
      // ここでリソースを解放
   }
}