ES2016(ES7)では非同期プログラミングが策定されました。各JavaScriptエンジンもPromiseから対応が始まり、Can I useを参照すると2018/1現在ではIE11以外のデスクトップ、スマホとも主要ブラウザはほぼ対応が完了していて利用出来る状態が進んでいるようです。
非同期関数からのエラーをどう返すべきか?
node.jsを開発を行っていて非同期プログラミングをしているとPromise配下では例外とrejectがあって何を使って良いのか考えていました。辿り付いた結論は、
- rejectはシステムでは極力使わずどうしても必要な場合は例外を利用する
- エラーハンドリングを行う必要がある想定可能なエラーはasync functionのreturnで結果を返す
ということになりました。どうしてこういう方針になったのかをそもそもエラーとは何かを考察しながら説明します。
システムが想定できるエラーと想定外に起こるエラー
例外とrejectがnode.jsでどのような挙動になるかの前に、そもそもエラーには種類があるということから再度ふりかえります。システムには以下のような想定出来るエラーと想定出来ないエラーがあります:
- 想定出来るエラーの例
- ユーザー操作で数値を入力して欲しいのに文字が入力された
- 開くファイルを指定されたがシステムに読み出し権限がなかったりファイルが無かった
- 想定出来ないエラーの例
- システム使用中にメモリオーバーフローとなった
- 設計上ではあり得ない値が渡されてきた。0で割った・正の値が欲しいのに負の値が渡された
システムの通常利用で起こりうる1.のエラーの場合、当然ユーザーにどう対処して欲しいかのメッセージを通知するなどエラーハンドリングする必要があります。
一方、想定外に発生する2.のエラーに関してはシステム外部が何らかの異常状態に陥っているか、設計の意図通りに実装出来ていない若しくは呼び出せていない、つまりバグが潜んでいるシステム内部の異常といえます。いずれの場合にもこれ以上正常の動作は期待出来ないので処理の継続をその場で中断して差し戻す、つまり例外をthrowすべきです。
async関数でのrejectの取り扱い
async / await は2016年10月にリリースされた V8 version 5.5 (Node.js 7.6) にて対応されており比較的新しい仕様です。Promiseはかなり前から対応しています。まず、Promiseのみ利用してChrome 63のデバッグコンソール上でrejectしてみます:
> promise = () => {
return new Promise((resolve, reject) => {
reject(new Error("Promise Error"))
})
}
> promise().then((result) => {})
Promise {: Error: Promise Error…}
VM8395:2 Uncaught (in promise) Error: Promise Error
at Promise (:2:12)
at new Promise ()
at promise (:1:26)
at :1:1
Promiseは第2引数にrejectを持ってエラー発生時に呼び出すことができ、やろうと思えば任意の値を返せてしまうのでエラーハンドリングに使えてしまいます。。
素のPromiseを利用すると非同期処理部分をPromiseで囲んでreturnする必要があってまだネストが多く可読性がいまいちです。async / awaitを利用すればほぼ同期処理と変わらない扱いになるのでよほどの理由が無ければこちらを利用すると思います。Promise同様のrejectを async / await で表現すると以下の様になります:
> promise = async () => {
throw new Error("Promise Error")
}
> let result = await promise()
VM772:3 Uncaught (in promise) Error: Promise Error Error: Promise Error
at promise (:2:11)
at :1:36
async関数とreturnでreturn Promiseとresolve呼び出しと同等の扱いになります。また、rejectを明示的に呼ぶ仕様は無くなり、例外をthrowすることで内部的にPromiseはrejectedで返されるようです(少なくともV8はそういう挙動になる模様)。
このように、ECMAScriptではasync / awaitの仕様策定の時点でrejectと例外は同等の扱いと解釈するようになったようです。想定外の異常系エラーを表現したい場合は非同期処理においても例外を投げれば良い様です。
想定可能なエラーの扱いについて
では、残るは想定可能なエラーをどう返せばということですが、async return (resolve)で返すようにすると良いです。以下はファイルオープンを非同期関数としたサンプルを書いてみます:
function async open(filepath) => {
fs.open(filepath, 'wx', (error, fd) => {
return !error
})
}
これで方針もすっきりしたように思います。もし、非同期関数内で標準ライブラリ等でAPIによってはファイルが無いや権限が無い等で想定されるエラーにあるのにも関わらず例外が返ってくる事もありますが、その時はtry-catchで想定するエラーをハンドリングしてエラー結果をreturnで返すのが良いでしょう。
まとめ
いかがだったでしょうか。もう一度まとめておくと、
- rejectはシステムでは極力使わずどうしても必要な場合は例外を利用する
- エラーハンドリングを行う必要がある想定可能なエラーはasync functionのreturnで結果を返す
です。ちゃんとreturn値で返してエラーハンドリングをちゃんとやりましょうというのはGo言語でも例外の代わりにpanicと表現を変えてより使うのは異常の時だけだよという雰囲気を出してさらに公式でエラーをreturnで返そうと言及もしており、ECMAScriptにおいても想定されるエラーなのかそうじゃないのかを意識してコードに反映していくのは良いことだと思います。この方針が気に入りましたら是非自分のプロジェクトに適用してみてください:)