neverthrow で局所的に Result 型を使い、 try-catch より安全に記述する
Result 型 (類似するものとして Either Monad の方が有名かもしれません) を導入する場合、アプリケーション全体の設計を変えたり、全箇所を書き換える必要はありません。
neverthrow は部分的に使用でき影響範囲も閉じるので、局所的に使い始めることができます。
(Rust のような) Result 型 とは
ざっくり言うと関数の処理の結果と成否を 1 つの型 Result<T, E>
で表したものです。(T は期待する結果の型、 E はエラーを表現する型)
筆者は詳しくはないのですが、 Haskell 等にある Either<L, R>
とは厳密には違うようです(Either は両方の値が使用可能であることを前提としている?)
参考: Rust ではなぜ、Either 型ではなく Result 型を採用しているのか
neverthrow とは
TypeScript で Result 型を実現するためのライブラリです。
supermacro/neverthrow
import { err, ok, Result } from 'neverthrow'
const fetchUser = async (): Promise<Result<User, Error>> => {
const response = await fetch(...)
if (response.ok) {
return ok((await response.json()) as User)
} else {
return err(new Error(...))
}
}
const fetchResult = await fetchUser()
if (fetchResult.isErr()) {
return fetchResult.error // <- Error 型である
}
return fetchResult.value // <- User 型である
このように、neverthrow を使用すると try-catch や throw を行うことなく手続き的にエラーを表現することができ、分岐による型の絞り込みも動作します。
neverthrow を局所的に採用する
上記のサンプルの通り、関数を Result 型を返すように変更し、その関数を呼び出す箇所で isErr()
の処理を入れるように回収することで、局所的に採用できます。
関数単位で変更できるため、冒頭の繰り返しになりますが、アプリケーション全体の設計を見直す必要などはありません。
neverthrow を採用すると何が嬉しいか
neverthrow を使うこと、ひいては try-catch をやめることで、以下の効能が得られると考えています。
- throw での大域脱出によるコードの追いにくさの解消
- そもそも throw しないことにより catch 漏れによる想定外挙動の回避
- 多段 catch によるコードや型の複雑さの解消
他にも考え方の面で色々と効能はあるかと思いますが、上記が主な効能かと思います。
例外を throw する Axios と相性が悪い
上記の例では fetch を使用しているため response.ok
を見て自身で処理を分岐していますが、 Axios は status code が 4xx 、 5xx だと例外を throw してしまいます。
catch してやれば良いのですが、 try-catch を入れるだけでもスコープの関係からコードは複雑になりがちなので、 validateStatus 等で throw しないようにするか、そもそも特別な理由がなければ Axios を採用しなくてもよいのではないかと筆者は考えます。
Discussion