TypeScriptのError Handling
大きく分けて2つの方針がある
この2つのどちらを採用するかで、かなり考えることが異なる
projectの全体を通して書き方が変わるので、割と大きな選択になる
一部を直せば移行できる、という感じでもない
例えば、UseCase層とRepository層の関数を考えた時、
try/catchなら両層の関数の返り値の型は正常系のみになるが
Eitherなら、両層全ての関数の返り値の型がEitherになる
みたいなイメージになる(かなり雑だけど)
後者の場合、関数内でEitherをhandlingすることになるので、1つ1つの関数の書き方も変わる
TypeScript/JavaScriptの言語思想的にはtry/catchを使ってerror handlingをするのが普通
JSの標準関数や、JSのlibraryの例外はthrowしてくるのが普通
例えば、JSON.parseは、引数をJSONに変換できなかった時にthrowする
例えば、axiosは、HTTP Responseが4XXや5XXの時にthrowする
catch節でErrorの条件分岐もできる
instanceofを使う
返り値の型は正常系のみになる
これは「Errorが生じる時のことを気にせずに書ける」といえば長所のように聞こえる
しかし実際は、「この関数ってthrowするんだっけ?」がわからなくて困る
JSON.parse()初見の人は、docsか内部実装を読む以外に「これは失敗時にthrowする」ということを知る術はない
全ての関数の仕様を理解していないと、全ての関数がthrowするものだとして捉える他なくなる
handlingを強制できないため、errorが起きてもそれをガン無視する実装になっているかもしれない
catchできたとしても、どこからthrowされたのかを特定するのも難しい
tryの中でどの種類のerrorがthrowされるかをある程度わかっていないといけない
型などでは明示されていないので、内部を読む以外に知る術はない
instanceofするにしてもerrorの種類がわからないと絞り込みようがない
分岐で絞り込めなかった場合は、もみ消すか、親に渡すかしかない
「try節は小さくしましょう」みたいな運用でカバーが必要になる layered architectureの場合は、層ごとのルールを設ければそこまで問題にはならないかも
運用でカバーは否めないがmrsekut.icon
EitherやMaybeのような型を使ってerrorを返しうるかどうかを表現する
関数の型を見れば、errorを返しうるかどうかの判別がつく
handlingを強制させるので、handling忘れも生じない
運用でカバーも必要ない
すごくシンプルmrsekut.icon
これはHaskellのような言語なら、比べるまでもなくこちらの方が良いだろうmrsekut.icon
ただ、TypeScriptでやるとなると問題が出てくる
JS/TSの思想がtry/catchである
Eitherのhandlingをする能力がしょぼい
思想がtry/catchであるということは、標準関数やlibraryはばんばんthrowしてくるということ
厳密にやるなら、throwをEitherに変換する関数をいちいち噛ます必要がある
噛ますのを忘れると、どこでもcatchしていないので、handlingできない
結局、Eitherのhandlingもthrowのhandlingも気にしないといけなくなる
handlingを扱う能力がしょぼいので、便利に扱う関数群が必要になる
例えば、Either<string, Post>を返す2つの関数f,gを考える
gは内部で、fを呼んでいる
fがerrorを返した場合、gはそれをそのまま返すが、その場合もconstructorを呼ばないといけない
code:ts
const g = (): Either<string, Post> => {
const r = f();
if (isRight(r)) {
return right({ id: 1, title: r.value.title });
}
return left(r.error);
};
declare function f(): Either<string, Post>;
至るところでhandlingしないといけなくなりうる
HaskellではEitherをMonadにすることで回避している
Eitherをmonadにするような仕組みがあると楽できる
そのためには、自分で作るか、libraryに頼るか、になる
例えばfp-ts
libraryを使う場合、project全体で使用するので、かなり依存することになる
monadっぽい書き方になるので、全体的に書き方が変わる
また、チームでやるなら一人は関数型に慣れた人が欲しい
結論
どっちもしんどいmrsekut.icon
try/catchを使うなら、
layer間でルールを設ける運用をするのが良さそう
Repository層の関数はthrowする、UseCase層がhandlingをする、など
運用でカバーになるが仕方ない
Eitherなどの型を使うなら、
べったりにはなるがlibraryを使うのが良いと思う
monadを使ったEitherの扱いを知っていると楽できる
libraryがthrowすることも常に頭の片隅に置き続ける必要がある