🎯

【Flutter】runZonedGuarded【よく理解せずに使っていたものたち】

2025/01/25に公開

Flutterアプリ開発でよく目にする以下のコード:

Future<void> main() async {
  //    ↓↓↓これ↓↓↓
  await runZonedGuarded(() async {
    // アプリを起動
    // 省略...
    runApp(const MyApp());
  }, (error, stackTrace) {
    // エラーハンドリング
    // 省略...
  });
}

runZonedGuardedの仕組みや目的を深掘りせずに「テンプレート」として使ってしまっていましたが一体何をしているのでしょうか。この記事では自分なりに調べて咀嚼した内容をまとめています。

runZonedGuardedとは?

runZonedGuardedはDartのdart:asyncライブラリに属する関数です。

意図的にゾーン (Zone) を作成することで、Dartの非同期処理で発生する未処理のエラーを効率的にハンドリングするために使用されます。

提供する機能について、公式ドキュメントから一部抜粋・意訳します。

  • bodyを新しいエラーゾーンで実行。
  • ゾーン内で発生した同期・非同期エラーをキャッチしてonErrorハンドラで処理。
  • 非同期エラーが別のゾーンに伝播しないように設計。

役割

漏れる非同期エラーのキャッチ

非同期処理のエラーはtry-catchを抜けた後に発生する可能性があるため、通常のエラーハンドリングではキャッチできません。
例えば、HTTPリクエスト中に未処理の例外が発生した場合、アプリがクラッシュする可能性があります。
このようなエラーをrunZonedGuardedでキャッチして適切に処理することで、アプリの安定性を保つことができます。

ログ収集との連携

runZonedGuardedを使うことで、エラーが指定したゾーン内で集中的に処理されるため、エラーの収集やログ出力が一箇所で行えます。FirebaseCrashlyticsなどのエラーログ収集ツールと組み合わせることで、未処理エラーを記録してデバッグやモニタリングに役立てられます。

(補足)ゾーンとは?

先ほどから何度も登場している「ゾーン」とは、一体何でしょうか。
ゾーンとは、言葉のイメージの通り、特別な部屋のようなものです。特定のエリアを囲うことでゾーンを作成し、中で起きたこと(エラーや動作)はそのエリアのルールに従って管理できるのです。

具体例

話を戻して、以下のコードでrunZonedGuardedの挙動を確認します:

void main() {
  runZonedGuarded(() {
    // ゾーンAで Future を作成
    final future = Future.error(Exception('ゾーンAで発生したエラー'));

    runZonedGuarded(() {
      // ゾーンBでゾーンAの Future を利用
      future.catchError((error) {
        print('ゾーンBでエラーをキャッチ: $error');
      });
    }, (error, stack) {
      print('ゾーンBで未処理のエラーをキャッチ: $error');
    });
  }, (error, stack) {
    print('ゾーンAで未処理のエラーをキャッチ: $error');
  });
}

出力結果

ゾーンAで未処理のエラーをキャッチ: Exception: ゾーンAで発生したエラー

詳しい解説

先ほどの例をゾーン毎に色分けすると以下のようになります。

ゾーンB内でfuture.catchErrorを使っていますが、future自体がゾーンAで作成されたものなので、そのエラーはゾーンAに属します。そのため、ゾーンBではエラーをキャッチできず、ゾーンAの未処理エラーハンドラが呼び出されます。

runZonedとの違いは?

runZonedGuardedを調べる中でrunZonedという存在を知りました。
runZonedもゾーンを作る関数ですが、公式ドキュメントを読むと、runZonedonError引数が非推奨で、代わりにrunZonedGuardedを使うように言われていました。

なぜでしょう。
非推奨のonError引数が渡された場合、runZonedrunZonedGuardedを使用してエラーをキャッチしようとします。ただし、型引数Rが非nullableの場合、エラーがスローされてnullが返る可能性があるのです。

何が起きてしまうかを以下のコードで説明します:

void main() {
  int result = runZoned<int>(
    () {
      throw Exception("エラーが発生");
    },
    onError: (error, stackTrace) {
      print("エラーをキャッチ: $error");
    },
  );

  print("結果: $result"); // 実行時エラーになる
}

このコードではRが非nullable型(int)ですが、runZoned内でエラーが発生するとnullを返します。戻り値が非nullable型である場合にnullを返すことはエラーの原因になります。

戻り値を使用しない設計であれば、この問題は起きません。
しかし、runZonedonErrorを指定する場合、内部的にrunZonedGuardedが呼び出される仕組みになっているため、最初からrunZonedGuardedを使うことが推奨されているというわけです。

runZoned自体は、エラーハンドリングを必要としない用途では依然として有用ですが、エラーハンドリングを行う場合は、常にrunZonedGuardedを使うべきです。

まとめ : runZonedGuardedとは

  • ゾーン機能の利用
  • 非同期処理のエラーハンドリング(アプリの安全性向上)
  • エラーの集中管理(エラーログ収集ツールとの連携)

Discussion