したかったこと
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を使って例外処理を一カ所で行う方法を解説しました。非同期処理内に全部例外処理を書かなければならないのか、と思いましたが、そんなことはなかったです。
参考
- [Flutter x Firebase] Crashlyticsと連携してクラッシュレポートを取得する
Zoneの考え方について参考になりました。2014年の記事のため、runZoneです。