TypeScriptへRustのようなResult型の導入をお勧めする記事や言説が多いので導入してみましたがあまりよくなかったです。という共有になります。
Result型を導入しても try-catch からは逃れられない
これに尽きます。
Result型を導入したあと、try-catch を末端に押しやってそこ以外はResult型のみの世界を実現しようと、おそらくみんな考える。でもそれは機能しない。
実際にやってみるとこんなコードが多発する。
function hoge():Result<void> {
try {
// fn()は綺麗な世界の実現のためResult型を返すようにしてある。この関数のようにね。
const result = fn()
// ここで例外が発生する処理が必要になる
return ok(result)
} catch(e) {
return err(e)
}
}
副作用部分だけ閉じ込めれば綺麗になるか
API呼び出しなどの副作用だけ閉じ込めればこんなコードは避けられると考えるかもしれないが現実は甘くない。
JavaScriptのさまざまな組み込み関数が例外を発生させるし、nullやundefinedへのアクセスも例外を発生させる。これらの多くはRustやGoで言うところのpanic扱いだから戦略を分けて、ユーザー定義のエラーだけResult型を使うという考え方はできる。それはある程度うまくいくだろうが、例外を発生させる外部ライブラリなどの導入が起きると破綻する。
Result型がうまく機能する場所はあるか
Server Actions
マッチするというか使わざるを得ないのはServer Actions。Server Actionsはセキュリティ上の理由からプロダクション環境においては例外の情報が削除される。例外を使ったエラーハンドリングは使い物にならない。
その他
他は思いつかない。
綺麗にtry-catchを末端に押しやれば使えるかもしれないが、TypeScriptの採用が多いフロントエンド開発においてはそこまで実装がレイヤー化されることは珍しいだろう。
バックエンド開発であれば、ユースケース層に近い部分では綺麗になるかもしれない。が、レイヤーによって戦術を変えてまで導入したいだろうか。チーム開発の場合、どこでtry-catchを使ってどこでResult型を使えばいいかわからなくなって余計な混乱を招きそう。
X(Twitter)上のResult型導入に対しての懸念
これ本当にみんないいと思って使ってるのか?と思ってXで検索したらやはり懸念を覚えている方が結構いらっしゃる。
まとめ
Server Actionsなど限定的にメリットが大きいケースはあるが、Result型の導入がクリーンな理想的な世界を生み出すことはない。
Goに入りてはGoに従えという言葉があります。
同じ話ではありますが、言語はエラーハンドリングを規定するのだと思い知らされました。