Node.jsのコード中でthrow
された場合、try-catchを入れないとuncaughtException
となる。
また、EventEmitterでon('error')
を定義していない場合もemit('error')
の内容がthrow
されるため、同様にuncaughtException
になる。
uncaughtException
が発生するとNode.jsはプロセスを終了する。
on('uncaughtException', err => {})
で検知はできる。が、回復は推奨されない。
どのようにエラーハンドリングすべきか考えてみる。
問題点
1. イベントハンドラでon('error')
を入れ忘れる
EventEmitterではプログラマがon('error')
を定義しない場合、例外をthrow
する。
(Node.jsではon('error')
を書きましょうと説明している)
このような例外を検知する方法として、前述したuncaughtException
を利用することができる。ただし回復は推奨されない。
回復が推奨されない理由として、イベントハンドラ内で例外が発生した場合に無限ループ処理になってしまうことを防ぐため、という説明がある。 以下Node.jsのドキュメントより抜粋。
Exceptions thrown from within the event handler will not be caught. Instead the process will exit with a non zero exit code and the stack trace will be printed. This is to avoid infinite recursion.
なぜイベントハンドラで発生した例外から回復するのがよくないか
確かにイベントハンドラ内で、不用意にcatchした例外から回復させるのは安全でない。 下記は超雑な例だが、想定外の状態のイベントを無理やり再実行するような書き方もできるといえばできる。 ただ、想定外の例外が発生したためにイベントの状態が予測できず、回復が難しいと思われる。
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.isStarted = false;
emitter.on('event', (val) => {
if (!emitter.isStarted) {
console.log('emitter was not started.');
emitter.isStarted = true;
emitter.emit('error', new Error('error event')); // same as throw Error
}
console.log('emitter was started. nothing to do.');
});
try {
emitter.emit('event', 'error!');
} catch (e) {
console.log('caught error! : ' + e);
emitter.emit('event', 'error!'); // error won't thrown.
}
2. ふつうにtry-catchを入れ忘れる
イベントハンドラとは関係なくtry-catchを入れ忘れるというしょうもないミスで、プログラマが想定していなかったthrow
が実はあり、ある時突然throw
された例外でプロセスがダウンするという状況は起こり得る。
依存しているライブラリ内で入れ忘れがあったりすると、全然想定していなかったプロセスダウンになりそう。
// 依存ライブラリの関数を呼び出し
somthing.call(); // 関数下でthrow Errorがあると、try-catchしていなければ当然プロセスダウンする
Promiseでtry-catch入れ忘れ問題に対処する
Promise内でthrow
されたエラーはcatch()
に渡される。
また、非同期処理でのエラーはreject()
に渡す、とされている。
The executor is expected to initiate some asynchronous work and then, once that completes, call either the resolve or reject function to resolve the promise’s final value or else reject it if an error occurred.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
function errFunc() {
throw new Error('wooooooooo!!');
};
const mypromise = new Promise((resolve, reject) => {
// 非同期処理のエラー
reject(err);
errFunc(); // throw new Error('err');
});
mypromise.then(value => {
console.log('promise is resolved. : ' + value);
})
.catch(err => {
console.log(err);
});
catch()
がない場合は、Promise内で発生したthrow
やreject()
は何のエラー後処理も行われない。uncaughtException
としても扱われない。
on('unhandledException')
で、catch()
で処理されなかったエラーを取得することができる。
非同期処理はPromiseを使って書く、という方針にすると、try-catch入れ忘れで想定外のuncaughtException
発生からのプロセスダウンの流れは避けられる。
※ on('error')
のように非同期でエラーイベントを取得した場合はreject()
し、throw
されるものと一緒にcatch()
で処理する。
catch()書き忘れ問題
try-catch書き忘れ問題についてはPromiseで解決できる。では、Promiseでcatch()
しなかったらどうなるかという、これもまたしょうもない問題を思いつく…。
catch()
を書き忘れて処理されていない例外は、on('unhandledRejection')
で拾うことができる。
on('unhandledRejection')
で、発生したcatch()
されない例外についてログを出力しておくことによって、とりあえず未処理の例外があるということには気付ける。
process.on('unhandledRejection', (reason, promise) => {
// handle rejection
});
function errFunc() {
throw new Error('wooooooooo!!');
};
const mypromise = new Promise((resolve, reject) => {
// 非同期処理のエラー
reject(err);
errFunc(); // throw new Error('err');
});
mypromise.then(value => {
console.log('promise is resolved. : ' + value);
})
まとめ
- しょうもないが、try-catch書き忘れや
on('error')
書き忘れでNode.jsのプロセスが落ちることがある - Promiseを使うと上記のような書き忘れで不用意にプロセスが落ちることを避けることができる