パルカワ2

最近はFlutterをやっています

Dartのエラーハンドリングのつらみ

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 がついてないのでどうしようもないという問題はある。

まとめ

自分的には型で解決できることは型で解決したほうがシンプルになると思うので型で解決したほうがよいと思う。そして、sealed classなエラーオブジェクトを返す形にすればそれ以降は適切にエラーハンドリングされている状態になるので何もないよりはまだ安心できるような気はする。 ただし、DartにはResult型はないので利用するならどのライブラリを利用するかという話になってくる。それはまた別の話ということでまた今度。

またSwiftはthrows句というのがあり便利だったなというのを思い出した。言語仕様としてできるようになっているのが一番良い。

Error Handling | Documentation