要点
クライアント開発において、独自の例外クラスを定義する場面はほとんどない。エラーは 例外ではなく Result で返し、状態として伝播させるのが適切である。
前回の記事では、Exception.message に UI テキストを入れるアンチパターンを扱った。
本記事では、その根本にある「エラーを例外で表現する」という設計そのものを見直す。
なぜ独自例外を作りたくなるのか
典型的な動機は以下の通りである。
- エラーの種別を区別したい — ネットワークエラーと認証エラーで処理を分けたい
- エラーに情報を付加したい — HTTP ステータスコードやエラーメッセージを保持したい
- サーバーがそう返してくるから — サーバーの例外構造をそのままクライアントに持ち込む
いずれも正当な要求だが、例外でなければ実現できない要求ではない。
例外の問題点
例外は本来、回復不能な異常事態 を通知する仕組みである。クライアント開発で扱うエラーの多くは、これに該当しない。
| エラーの種類 | 性質 | 対応 |
|---|---|---|
| ネットワーク接続失敗 | 想定内 | リトライや代替表示 |
| 認証トークン期限切れ | 想定内 | 再認証フローに遷移 |
| バリデーションエラー | 想定内 | UI にフィードバック |
| JSON パース失敗 | 想定内 | サーバー仕様変更の調査 |
| NullPointerException | 異常 | プログラムのバグ。即修正 |
想定内のエラーを throw で飛ばすと、以下の問題が生じる。
- 制御フローが不透明になる — どこで catch されるか、コードを追わないとわからない
- 型安全性がない — 関数のシグネチャにエラーの可能性が表れない 1
- CancellationException の誤処理 — コルーチンのキャンセルを握りつぶすリスクがある
Result で返す
Kotlin の Result<T> を使えば、成功と失敗を型で表現できる。
|
|
呼び出し側は onSuccess / onFailure で分岐する。try-catch は不要である。
|
|
runCatchingOrCancel
標準の runCatching は CancellationException も捕捉してしまう。コルーチンのキャンセルを握りつぶさないためのラッパーが必要である。
|
|
エラーの種別を区別する
「Result だとエラーの種別を区別できないのでは?」という疑問がある。Result.failure の中身は Throwable なので、型で分岐できる。
|
|
ここで使っているのは IOException や HttpException — ライブラリが定義した既存の例外 である。独自例外を定義する必要はない。
より構造化したい場合は、sealed class でエラー型を定義する方法もある。
|
|
ただし、これは例外ではなく データ である。throw するものではなく、状態として保持する。
まとめ
クライアント開発で扱うエラーの大半は想定内であり、例外ではなく 結果型(Result など)で返す のが適切である。エラーは throw で飛ばすものではなく、状態として伝播させる。
- 独自例外でなければ実現できない場面は、クライアント開発においては少ない
- 独自例外を定義する前に、まず結果型で表現できないかを検討する
- 本当に
throwでなければいけないのかを考える
次の記事では、Result と組み合わせたローディング管理を扱う。
-
Kotlin には checked exception がない。関数が何を throw するかは、ドキュメントを読むか実装を追うしかない。 ↩︎