【Flutter】非同期処理内で発生した例外を、runZoneGuardedを使って例外処理する

したかったこと

FlutterでFirebaseAuthの認証をしようとプログラムをしていた。認証時にメールアドレスやパスワードが異なると、当然だが認証に失敗し、例外が発生する。そして、その例外をキャッチして適切なエラーメッセージを画面に表示したかったのだが、できなかった。
認証処理が非同期で実行されており、その中で例外が発生しているため、try-catchで例外を処理しようとしても、内部で例外が処理されてしまって、例外を自分で処理することができなかった。

結論

try-catchではなく、runZoneGuardedで処理をする

改善前:try-catchの例

以下テストコードとして、駄目だった場合を記述する。

test('Future try-catch', () async {
  Completer completer = Completer();

  try {
    final future = Future(() {
      print('1');
    }).then((_) {
      print('2');
      Future(() {
        throw Exception('7(error)');
      });
    });
    print('3');
    await future;
    print('4');
  } catch (error, stack) {
    print('5');
    print(error);
    completer.complete();
  }

  print('6');
  await completer.future;
});

期待としては、「5」と例外の内容を出力したかった。しかし、そうはならない。非同期処理内で発生した例外をtry-catchを使って処理しようとすると、以下のように勝手にスタックトレースを出力されて自分で処理できなかった。

C:\src\flutter\bin\flutter.bat --no-color test --machine --start-paused --plain-name "Future try-catch" test\async_error_test.dart
Testing started at 14:27 ...

3
1
2
4
6
test\async_error_test.dart 90:11  main...
===== asynchronous gap ===========================
dart:async                        new Future
test\async_error_test.dart 89:9   main..
===== asynchronous gap ===========================
dart:async                        Future.then
test\async_error_test.dart 87:10  main.

Exception: 7(error)

改善後:runZonedGuardedの例

そこで調査すると、runZonedが良いらしいが、そちらはDeprecatedになっている。後任は、runZonedGuardedらしいので、以下のようにtry-catchを書き換える。

test('Future runZoneGuarded', () async {
  Completer completer = Completer();

  runZonedGuarded(() async {
    final future = Future(() {
      print('1');
    }).then((_) {
      print('2');
      Future(() {
        throw Exception('7(error)');
      });
    });
    print('3');
    await future;
    print('4');
  }, (error, stack) {
    print('5');
    print(error);
    completer.complete();
  });

  print('6');
  await completer.future;
});

上記のようにrunZonedGuardedに書き換えると、自分のしたい例外処理が実施できた。ここでは出力されるだけだが、実際のアプリではスナックバーに出したり、ダイアログに出したりできる。

3
6
1
2
4
5
Exception: 7(error)

runZonedGuardedとはなにか

Futureの非同期処理はZoneという範囲内で行われ、ZoneがFuture毎に区別されるので、Future内の例外とtry-catchが別のZoneにあるから、処理してくれない。しかし、runZonedGuardedを使うと、それを一緒のZoneで扱ってくれるから、非同期処理内の例外も同じ場所で一括処理できる、らしい

以前Crashlyticsを使ったときに、アプリの大元でrunZonedGuardedを使っていましたが、ようやく意味がなんとなく分かりました。

まとめ

とういことで、非同期処理があっても、runZonedGuardedを使って例外処理を一カ所で行う方法を解説しました。非同期処理内に全部例外処理を書かなければならないのか、と思いましたが、そんなことはなかったです。

参考