まつちよの日記

プログラミングに関する知見や、思ったことを書き残します。

新しいFlutterのエラーハンドリング方法

tags: Flutter

※ 最新情報は、Flutter公式のHandling errors in Flutterや、Crashlyticsをご利用ならFirebaseCrashlyticsを使い始めるを見るのをおすすめします。

久しぶりにFlutterのドキュメントHandling errors in Flutterを見たら、Flutterのエラーハンドリング方法が新しくなっていました。 以前の方法のrunZonedGuarded()がなくなっているけど大丈夫なの?と不安になったので、どう変わったのか整理してみました。

結論

  • Flutterが発生させるエラーはこれまで通りFlutterError.onErrorでハンドリングする。
  • runZonedGuarded()やIsorate.current.addErrorListener()でハンドリングしていたエラーは、PlatformDispatcher.instance.onErrorでハンドリングできるようになった。

新しい方法

Flutter 3.3からの新しい方法は以下の通りです。 Handling errors in Flutterほぼそのままです。

この例ではクラッシュレポートの送信にCrashlyticsを利用しています。Crashlyticsについては深くは触れません。FirebaseCrashlyticsを使い始めるをご参照ください。

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  final defaultFlutterErrorHandler = FlutterError.onError;
  FlutterError.onError = (errorDetails) {
    defaultFlutterErrorHandler?.call(errorDetails);

    final isSilent = errorDetails.silent;
    FirebaseCrashlytics.instance.recordFlutterError(errorDetails, fatal: !isSilent);
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

↑runZonedGuarded()でハンドリングするのではなくPlatformDispatcher.instance.onErrorでハンドリングするようになっています。

以下によると、runZonedGuarded()でcustom zoneを作る方法はパフォーマンスに影響するため、Flutter3.3ではこの方法になったようです。

custom Zones were detrimental to a number of optimizations in Dart’s core libraries, which slowed down application start-up time. https://medium.com/flutter/whats-new-in-flutter-3-3-893c7b9af1ff

以前の方法

下記、以前の方法です。 FlutterFireの現時点でアーカイブになっているUsing Firebase Crashlyticsにはこの記載が残っていますね。

void main() {
  runZonedGuarded<Future<void>>(() async {
    WidgetsFlutterBinding.ensureInitialized();
    Firebase.initializeApp();

    if (kDebugMode) {
      await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false);
    }
    
    final defaultErrorHandler = FlutterError.onError;
    FlutterError.onError = (FlutterErrorDetails errorDetails) {
      defaultFlutterErrorHandler?.call(errorDetails);

      final isSilent = errorDetails.silent;
      FirebaseCrashlytics.instance.recordFlutterError(errorDetails, fatal: !isSilent);
    };

    runApp(MyApp());

  }, (error, stackTrace) {
    FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true);
  });
}

↑runZonedGuardedで囲まれています。

補足: runZonedGuarded()について

この中で発生しハンドリングされなかった同期のエラーと非同期のエラーをハンドリングできるようになります。

The onError function is used both to handle asynchronous errors by overriding ZoneSpecification.handleUncaughtError in zoneSpecification, if any, and to handle errors thrown synchronously by the call to body. https://api.flutter.dev/flutter/dart-async/runZonedGuarded.html

気になったので動かしてみたところ、build()内やButtonのonPress()内で以下のような形で発生したエラーがrunZonedGuarded()のonErrorに来ます。

final future1 = Future.value(1).then((value) => throw 'Error in main() future'); // await&try-catchしない。catch()もしない。

補足: Isolate.current.addErrorListenerについて

上のアーカイブされたドキュメントのErrors outside of FlutterにIsolate.current.addErrorListenerというのがありました。 確認した範囲だと、下記の通りmain()内でthrowされたErrorをハンドリングできるようになるみたいです。

これも、Flutter3.3からは、PlatformDispatcher.instance.onErrorでハンドリングできるようになっていました。

void main() {
  Isolate.current.addErrorListener(RawReceivePort((pair) async {
    final List<dynamic> errorAndStacktrace = pair;
    print('***** Isolate.current.addErrorListener: ${errorAndStacktrace.first}');
  }).sendPort);

  runApp(const MyApp());
  
  throw 'Error'; // これが上のIsolate.current.addErrorListener()にくる。
}

おわりに

まとめとしては、「結論」と同じですが、FlutterError.onErrorとPlatformDispatcher.instance.onErrorを使うということになります。