はじめに
JavaScript あるいは TypeScript で 型アノテーションと合わせて例外が発生する可能性を明示することはできるのでしょうか?
関数が例外を返すことが分からないとその例外をちゃんと補足できるかわかりません。VSCodeなどのIDEに例外を認識させるにはどうしたらよいか、またその必要が本当にあるか紹介します。
結論
JavaScript、あるいはTypeScriptにおいては、「その関数が例外を投げる可能性があるかどうか」をコード内で表現することは非常に難しいです。
むしろ「例外はあるもの」として基本考える方が健康的かと個人的には思っています。
(※ ここでの「例外」とは JSにおいて `throw` されるエラー、およびそのシチュエーションを指します )
例外は、ある
初心者のうちは、まるで例外なんて存在しないかのようなコードを平気で書いてしまいます。
以下は、16進数を10進数に変換するための関数です。
const hexToDec = (hex) => {
return parseInt(hex, 16);
}
何が問題か
上記の関数には例外を発生させるポイントがいくつかあります。
- 引数 hex に 16進数以外が渡されてしまう可能性
- 引数 hex の型が string ではない可能性 (16進数はnumber型でも表現できる)
- 引数 hex の型が string だったとして、そこにスペースが含まれている可能性(hexのフォーマット)
…少なくとも、引数に渡された値が有効かどうかをチェックした方が良さそうです。
例外を、投げる
引数をチェックして、問題がある場合は例外を投げるよう修正しました。
const hexToDec = (hex) => {
if (!isHex(hex)) {
throw new Error("the given string is not hex.");
}
return parseInt(hex, 16);
};
ここで心配なのが、投げた例外がちゃんと `catch` してもらえるのか、ということです。
例外を知らせる術はあるのか
例えば VSCodeなどIDEは、関数の型情報を教えてくれます。上記の関数の場合、以下のような型になります。
hexToDec: (hex: string) => number
↑ をみただけでは、 `hexToDec` が例外を投げる可能性がある、ということは分かりません。チームメンバーは例外をcatchできるでしょうか?
JavaやSwiftの検査例外
JavaやSwiftには例外チェックの機構が含まれており、例外を発生させる実装をする場合はエラーハンドリングを強制します。流石に強制とまではいかなくても、その可能性くらいは実装者に知らせたいと思うわけです。
Promiseを返す?
例えば、`Promise`を返却することで .catch句 の必要性を知らせる実装はどうでしょうか?
hexToDec: async (hex: string) => Promise<number>
この場合確かに、戻り値を処理する場合は Promise
を解決する必要があるので、関数の使用者は Error の可能性を感じ取ってくれそうです。
しかしながら、個人的にこの実装は避けたいと思っています。 10進数への変換自体は同期処理できるはずであり、仮にエラーが発生する場合はほぼ間違いなくプログラマの実装ミスだからです。
2種類のエラーを考える
「Operational Error」と「Programmer Error」という有名なエラー分類があります。
Operational Error | ファイルアクセスしたけどファイルがない等のシステム依存のエラー。 実装自体には問題がない。 |
Programmer Error | 引数に与えるべき値を与えていない、配列長以上のアクセス等のプログラマ依存のバグ。 実装に問題がある。 |
このうち、「Operational Error」は Promiseでハンドリングすることが適切である可能性が高いです。一方で、「Operational Error」は Promiseではなく単に例外処理が適切と考えられます。
例外を知らせるには ドキュメントを書くしかない
つまり、例外自体は JavaScriptを使用する上ではどこでも発生する可能性があり、わざわざIDEからの支援がなくても当然関数がカバーしているべきと考えるしかないかと思います。
JSには検査例外のコンセプトはなく、例外処理を強制したり教えてくれる機能はありません。
JSDocに書く
最終手段として、JSDocがあります。ここに書いておけば、一応IDEから例外の可能性を見ることだけはできます。(自動的なお知らせや警告は当然ありません)
/**
* Converts the given hex string to a number.
*
* @param str The string to convert.
* @returns The resulting number.
* @throws Error if `string` isn't valid hex.
*/
const hexToDec = (str: string) => {
if (!isHex(str)) {
throw new Error("the given string is not hex.");
}
return parseInt(str, 16);
};
ないよりは幾分マシでしょう。
まとめ
- JavaScriptにおいて「例外」とは 実装上のバグに使用すべきである
- 実装上のバグはあらゆる箇所に存在する可能性があり、明示するものでなくてもよい
- 明示したければ JSDocが最終手段としてある
- 実装上のバグではないエラーは予測できるという意味で「例外」ではなくPromiseで処理できる
おまけ
以下は関連する内容について、補足です。
投げた例外は遠くまで飛んでいけ
そもそも、`hexToDec` 関数が例外を発生させることがわかったとして、使用者はこれをいちいち try-catch
するかというと、そうでもないかと思います。投げた例外は、なるべく遠くでキャッチできるような設計に全体としてなっているのが理想的だと思います。
エラーの種類が区別できてないから
先述のエラーの種類がちゃんと区別できていれば、「例外」とは本当の意味での例外であり、キャッチしたとしても何かカバーできたり、リトライで処理できるようなものではないことがわかります。
アプリケーションができることはぜいぜい「予期しないエラー」ダイアログを表示することくらいです。
…とするとこの例外をキャッチするのはアプリケーション全体として設定された catch句あるいはエラーバウンダリであり、`hexToDec` の呼び出し付近でのハンドリングはほぼ不要でいいかもしれないわけです。
例外を投げない言語が増えている
- 例えばGO言語には「例外を投げる」という概念は存在せず、ただの戻り値として扱います。
- また、RustではResultなどパターンマッチを用いて処理できる型として処理を行います。
他にもあるようです。
try-catchは複雑性を高める?
公式ドキュメントになぜ例外を推奨しない言語設計にしているのか、その理由が書いてあります。
Why does Go not have exceptions?
[なぜGoには例外がないのですか?]We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.
[例外をtry-catch-finallyのような制御構造に結びつけることは結果的に複雑なコードにつながると考えております。そして、それはファイルを開くのに失敗したなどの数多くの普通のエラーに例外としてラベル付することをプログラマーに推奨するものです。]Frequently Asked Questions (FAQ) – The Go Programming Language
遠投すればするほどどこに行ったか分からなくなる
「例外の catch は、なるべく遠くで」という設計にしないと、関数呼び出しのたびに不要な try-catchで通しの非常に悪いコードになってしまいます。
その一方で、遠くに飛んでいった例外は思いもよらない副作用を持ってしまったり、遠く離れた場所で処理される関係上、コードを追うのが難しくなってしまいます。
それはファイルを開くのに失敗したなどの数多くの普通のエラーに例外としてラベル付することをプログラマーに推奨するものです。
とあるように、Operational Error が例外として処理されてしまう懸念も依然としてあります。つまり実装上の問題ではない、予測できるエラーも「例外」としてしまう問題ですが、これは先述した通り Priomise化することで 一部解決が見込めます。