Node.jsでUnhandled Rejectionsのときにexis statusが0となる問題を回避する
Node.jsでUnhandled Rejectionsが発生してprocessが終了すると、Exit Statusが0
となる問題とその対策についてのメモです。
- 追記: Node.js 15+からUnhandle Rejectionが発生するとプロセスがExit Status 1で終了する動作がデフォルトとなりました
- Node.js v15ではunhandled rejectionでプロセスがエラー終了する
事前知識: Async FunctionはPromiseを返す関数定義です。 その辺について詳しくは次のサイトを読んでください。
今回のサンプルコードは次のリポジトリにあります。
Unhandled RejectionsとNode.jsの終了ステータス
次のmain.js
はPromise内で例外が投げていますが、呼び出し元のmain()
は何もキャッチしていません。
そのため、Unhandled Rejectionsが発生しています。
main.js
:
function waitFor(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function main(shouldFailed) {
await waitFor(1000);
throw new Error("ERRRRRRR!"); // failed after 1sec
}
main();
ブラウザではUnhandled Rejectionsは自動的にコンソールエラーとして表示されるためあまり問題には感じないかもしれません。
Node.jsではこのコードを実行するとUnhandled Rejectionsとなりエラー表示はでますが、その際のExit Statusは0
であるため正常終了となります。
$ node main.js
(node:12236) UnhandledPromiseRejectionWarning: Error: ERRRRRRR!
at main (/unhandled-reject-example/main.js:9:11)
(node:12236) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:12236) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
$ echo $?
0
DEP0018: Unhandled promise rejectionsの将来的にexit statusは0
ではなくすというDeprecationは出ていますが、Node 12時点でもデフォルトのexit statusが0
のままです。
Unhandled Rejectionsでプロセスが終了すると正常終了とみなされて、Exit Statusを見るツールがうまく動かないことがあります。 そのため、必ずなにかしらの方法でPromise/Async Functionの例外はキャッチすることが重要です。
Promise#catch
で明示的にエラー終了する
先ほどのコードを簡単に修正するなら、main()
の呼び出しに対してちゃんとPromise#catch
でエラー時の処理を書くことです。
fixed-main.js
:
function waitFor(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function main(shouldFailed) {
await waitFor(1000);
throw new Error("ERRRRRRR!"); // failed after 1sec
}
// Random Success or failure
main().then(() => {
console.log("ok");
// It is optional - if comment out is, node.js get same result
process.exit(0);
}).catch(error => {
console.error(error);
process.exit(1);
});
この場合は、main()
関数でエラーが発生(Async Function内でエラーをthrowするとRejectedなPromiseを返す)した場合にcatch
メソッドのコールバックが呼び出されます。
catch
内でprocess.exit(1)
で明示的にExit Statusを1
にしてプロセスを終了させているため安全です。
$ node fixed-main.js
echo Error: ERRRRRRR!
at main (/unhandled-reject-example/fixed-main.js:9:11)
$ echo $?
1
基本的にアプリケーションのエントリポイント呼び出しではエラーが起きた場合の処理は書いておくのが良いでしょう。
unhandledRejection
イベントで明示的にエラー終了する
Nopde.jsではprocess.on("unhandledRejection", (reason, promise) => {})
というイベントをサポートしています。
これは、Unhandled Rejectionsが発生した際に呼ばれるコールバック関数を登録できます。
このunhandledRejection
イベントを使えば、Unhandled Rejectionsが発生した際にProcessをエラー終了させることもできます。
fixed-unhandledRejection-event-main.js
:
process.on('unhandledRejection', (reason, promise) => {
console.error(reason);
process.exit(1);
});
function waitFor(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function main(shouldFailed) {
await waitFor(1000);
throw new Error("ERRRRRRR!"); // failed after 1sec
}
main();
このコードを実行してみると exit status が 1
となることがわかります。
$ node fixed-unhandledRejection-event-main.js
Error: ERRRRRRR!
at main (/fixed-unhandledRejection-event-main.js:14:9)
$ echo $?
1
これと同様のことをするモジュールはいくつか存在します。
- sindresorhus/hard-rejection: Make unhandled promise rejections fail hard right away instead of the default silent fail
- sindresorhus/loud-rejection: Make unhandled promise rejections fail loudly instead of the default silent fail
- mcollina/make-promises-safe: A node.js module to make the use of promises safe
まずはPromise#catch
で管理することが重要ですが、書き忘れた際にUnhandled Rejectionsが発生してしまうので、それを防止する役割として利用できます。
--unhandled-rejections=strict
でエラー終了させる
Node.js 12から--unhandled-rejections=mode
というコマンドライン引数が追加されています。
--unhandled-rejections=strict
でNode.jsを実行すると先ほどのUnhandled Rejectionが自動的にuncaught exceptionとして例外を投げるようになります。そのため、Unhandled Rejectionsがエラー終了となります。
$ node --unhandled-rejections=strict main.js
/unhandled-reject-example/main.js:9
throw new Error("ERRRRRRR!"); // failed after 1sec
^
Error: ERRRRRRR!
at main (/unhandled-reject-example/main.js:9:11)
$ echo $?
1
おわりに
基本的にはUnhandled Rejectionが発生しないようにPromise#catch
などをしてちゃんとエラーハンドリングしたコードを書くことが大切です。
それでも、意図しない例外などは発生するので、unhandledRejectionやuncaughtExceptionでのエラー時の処理を入れておくのが良い気がします。
Async Functionを使って簡単にPromiseを扱えるようになったので、このUnhandled Rejectionsの問題をよく見るようになった気がするのでこの記事を書きました。
サンプルリポジトリ:
Node.jsではEventEmitter.captureRejections
など新しいオプションが増えたりしているので、興味がある人はこの辺も見てみると良い気がします。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。