Promiseでエラーハンドリングのどういうところを解決できるのか?

Jul 6, 2016 ( Feb 11, 2022 更新 )

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内で発生したthrowreject()は何のエラー後処理も行われない。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を使うと上記のような書き忘れで不用意にプロセスが落ちることを避けることができる
Return to top