Dartでエラーハンドリングを行っているとtry-catchを使うことになるが自分的に困ったことがあるのでどうするべきか考える。
つらみポイント
なにがつらいかというとこの2点がつらい。他にも try {} catch (err, stack) {}
とするとErrorまでもcatchしてしまうとかもあるがLinterで防げる。 avoid_catches_without_on_clauses | Dart
エラーハンドリングを忘れる
Exceptionが出るのにtry-catchするのを忘れてしまうのを防ぐことができない。
エラーの網羅性検査ができない
どういうExceptionが起きるのか、エラーハンドリング時に網羅性を検査できない。
対処方法
Result型
Flutterの公式ドキュメントにもResult<Success>
型を使うように記載されている。Flutterの公式ドキュメントに記載されている方法は、エラー時にExceptionを返す型になっており、エラーハンドリングを忘れることは防げるが、エラーの網羅性の検査については問題を抱えたままになる。
Error handling with Result objects | Flutter
これに対しては、例えばUseCaseが返すResultのエラーをsealed classのエラーオブジェクトにしてあげるとUseCaseが想定しているエラーに関しては網羅性の検査はできる。一方で、結局はExceptionを変換しているだけなので完全に網羅的かと言われると違う。
sealed class HogeUseCaseException { static HogeUseCaseException from(Exception exception, StackTrace stackTrace) { if (exception is IOException) { return HogeIOException(); } ...; } } final class HogeIOException extends HogeUseCaseException {} class HogeUseCase { Result<Success, HogeUseCaseException> execute() { try { return Ok(...); } on Exception catch (err, stack) { return Err(HogeUseCaseException.from(err)); } } }
void callUseCase() { final result = hogeUseCase.execute(); switch (result) { case Ok: ...; case Err: switch (result.error) { // 網羅性の検査ができる case HogeIOException: ...; } } }
Linterで頑張る
DCM - Code Quality Tool for Flutter Developersを使う方法もある。
@Throws
というアノテーションを利用して、このアノテーションがついてるメソッドのエラーハンドリングをしていなかったらLintエラーにするとかそういうことができる。網羅性についてはThrowsに渡す型をちゃんと指定すればできそうだが、ちゃんと指定することを強制できるのかは知らない。
また、これも自分たちが変更できる範囲の話で例えばライブラリや自動生成されたAPI Clientには @Throws
がついてないのでどうしようもないという問題はある。
- prefer-correct-throws | DCM - Code Quality Tool for Flutter Developers
- handle-throwing-invocations | DCM - Code Quality Tool for Flutter Developers
まとめ
自分的には型で解決できることは型で解決したほうがシンプルになると思うので型で解決したほうがよいと思う。そして、sealed classなエラーオブジェクトを返す形にすればそれ以降は適切にエラーハンドリングされている状態になるので何もないよりはまだ安心できるような気はする。 ただし、DartにはResult型はないので利用するならどのライブラリを利用するかという話になってくる。それはまた別の話ということでまた今度。
またSwiftはthrows句というのがあり便利だったなというのを思い出した。言語仕様としてできるようになっているのが一番良い。