⚠️

neverthrow で局所的に Result 型を使い、 try-catch より安全に記述する

2024/03/18に公開

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(response)
  } else {
    return err(new Error(...))
  }
}

const fetchResult = await fetchUser()

if (fetchResult.isErr()) {
  console.error(fetchResult.error) // <- Error 型である
}
console.log(fetchResult.value) // <- User 型である

このように、neverthrow を使用すると try-catch や throw を行うことなく手続き的にエラーを表現することができ、分岐による型の絞り込みも動作します。

neverthrow を局所的に採用する

上記のサンプルの通り、関数を Result 型を返すように変更し、その関数を呼び出す箇所で isErr() の処理を入れるように回収することで、局所的に採用できます。
関数単位で変更できるため、冒頭の繰り返しになりますが、アプリケーション全体の設計を見直す必要などはありません。

neverthrow を採用すると何が嬉しいか

neverthrow を使うこと、ひいては try-catch をやめることで、以下の効能が得られると考えています。

  1. throw での大域脱出によるコードの追いにくさの解消
  2. そもそも throw しないことにより catch 漏れによる想定外挙動の回避
  3. 多段 catch によるコードや型の複雑さの解消

他にも考え方の面で色々と効能はあるかと思いますが、上記が主な効能かと思います。

例外を throw する Axios と相性が悪い

上記の例では fetch を使用しているため response.ok を見て自身で処理を分岐していますが、 Axios は status code が 4xx 、 5xx だと例外を throw してしまいます。
catch してやれば良いのですが、 try-catch を入れるだけでもスコープの関係からコードは複雑になりがちなので、 validateStatus 等で throw しないようにするか、そもそも特別な理由がなければ Axios を採用しなくてもよいのではないかと筆者は考えます。

関連: Axios より fetch の方が良いと思う

Discussion