【Dart/Flutter】非同期処理を含めた例外処理

はじめに

Flutter開発で非同期処理を実行しているときの例外処理がよく分かっていなかったので、今回改めてまとめてみました。
他の記事を読むと、「非同期処理ではawaitで例外処理を適切に扱いましょう」とありましたが、非同期処理を待つ必要性がないケースなどが考慮されてないと思う。また、Future からの then, onError, catchExceptipn が理解できていない。asyncやawait の有無が例外でどのように影響を与えるか。
そこで、色々なケースを検討したく、網羅してみました。

実際のユースケースとしては、外部パッケージを使って画面の明るさ調整を行いつつ画面を表示するが、例外が発生するかもしれない。明るさ調整に失敗しても、ハードウェア都合でユーザもアプリも(私も)どうしようもないので、処理は続ける。ただ処理に失敗した事実は、一応ユーザーに知らせたい。そのようなケースで適切に処理を行うのに、非同期処理と例外処理をどのように構成するのが良いか検討が必要になり、今回このようにまとめました。色々とまとめましたが、まだ理解できていないところもあるので、参考程度にお願いします。

非同期処理をなぜ非同期処理として扱うか

単純にパフォーマンスです。そちらについては、以下の記事に記載してるのでご覧ください。

【Dart】非同期処理の最適化について考える

わずか9ヶ月前ですが、「AI同士が通信する」ということに疑問を感じていました。しかし、今ではAIエージェントという形で自分でも活用してます(笑)
「LLMの返信待ち」=「非同期処理」のため、いまではLLMの回答を効率よく渡すために、それぞれのメソッドを「非同期で実装する」ということを意識するようになってます。

例外処理の適切な対応方法(多分)

とりあえず、以下の方針で良いかと思われる。保証は全くしない。ケースバイケースですし。

  • 同期処理
    戻り値がFuture型でも、asyncが付いていなければ、同期処理みたい。
    実行するメソッドにasyncがついていない
    可能ならこちらにする(データが必要になるタイミングまで取得待機を遅らせられるから)

    • try-catchを使用する。または大きな範囲でrunZonedGuardedを使う
  • 非同期処理(実行するメソッドにasyncがついている)

    • 非同期処理と見せかけて、中身でawaitしてほとんど同期処理の場合
      • catchErrorを使用する
    • 後続処理を続ける(正常実行されたか確認せず、例外が発生したら後で処理する)
      • 非同期処理の実行時にawait は使わない
      • onErrorrunZonedGuarded を使用する
    • 処理完了を待機し、後続処理を続ける(正常実行されたか確認する必要がある)
      • await を使って、非同期処理完了を待つ
      • 特定のメソッドにはonError、ある程度範囲のあるときはtry-catchを使う

今回のユースケース「明るさ調整をして、失敗したらダイアログを表示する」というのであれば、以下のような対応で良いと思われる。

  • 非同期処理にawaitは使わない(例外が発生しても、動作は変わらない)
  • onError内でダイアログを表示させる(例外が発生しようなメソッドが分かっていて、例外が発生したら処理をする)

例外処理のパターンと検証

0. テストのためのクラスとメソッド

/// 他の例外と区別するための例外クラス
class MyException implements Exception {}

/// 同期処理での例外処理のテスト
/// `willThrowException` が true の場合に MyException を投げるメソッド
/// そうでない場合は文字列 "success" を返す
String throwException(bool willThrowException) {
  if (willThrowException) {
    throw MyException();
  }
  return 'success';
}

/// 非同期処理でasyncのあるときの例外処理のテスト
/// 非同期で動作し、`willThrowException` が true の場合に MyException を投げるメソッド
/// そうでない場合は文字列 "success" を返す
Future<String> throwExceptionWithFuture(bool willThrowException) async {
  if (willThrowException) {
    throw MyException();
  }
  return 'success';
}

/// 非同期処理でasyncのないときの例外処理のテスト
/// 非同期を返すが、メソッド内部では `await` を使用していない例
/// `willThrowException` が true の場合は MyException を投げる
/// そうでない場合は Future で "success" を返す
Future<String> throwExceptionWithFutureOnly(bool willThrowException) {
  if (willThrowException) {
    throw MyException();
  }
  return Future.value('success');
}

/// メソッド内部で throwExceptionWithFuture を呼び出すが、`await` していない例
/// そのため、内部の例外がスローされるタイミングが直ちに確定しない可能性がある
/// ここでは常に "success" を返すが、実際には非同期処理の例外に注意が必要
Future<String> throwWithoutAwaitExceptionIn(bool willThrowException) async {
  throwExceptionWithFuture(willThrowException);
  return 'success';
}

/// メソッド内部で throwExceptionWithFuture を `await` する例
/// 例外がスローされた場合、即座に検知してエラーとして処理される
/// await による例外処理がなければ、"success" を返す
Future<String> throwWithAwaitExceptionIn(bool willThrowException) async {
  await throwExceptionWithFuture(willThrowException);
  return 'success';
}

1. 同期処理の例外

ケース 値の取得 例外発生 適切な処理方法
通常(例外なし) 取得できる 発生しない そのまま expect() で結果を確認
例外発生(同期処理) 取得できない 発生する try-catch で例外を補足
String throwException(bool willThrowException) {
    if (willThrowException) {
      throw MyException();
    }
    return 'success';
}

test('通常', () {
  expect(throwException(false), 'success');
});

test('同期処理での例外', () {
  try {
    throwException(true);
    fail('cannot reach');
  } on MyException catch (_) {
    // ここを通る
  } catch (e) {
    fail('different exception');
  }
});

2. 非同期処理(Future)の例外

  • 2-1. async のついた Future メソッド
  Future<String> throwExceptionWithFuture(bool willThrowException) async {
  • 2-2. async のつかない Future メソッド
  Future<String> throwExceptionWithFutureOnly(bool willThrowException) {

2-1. async のついた Future メソッド

ケース 値の取得 例外発生 適切な処理方法
通常(例外なし) 取得できる 発生しない そのまま expect() で結果を確認
await なし 取得できる(onError の値) 発生する .onError() で例外処理
try-catch使用時 取得できない 発生する runZonedGuarded で例外を補足
await あり 取得できない 発生する try-catch または .onError() で処理

thenを使用すると、非同期後の処理を設定できるのに加えて、戻り値を異なる値に変えることができる。

test('通常', () async {
  expect(await throwExceptionWithFuture(false), 'success');
  expect(await throwExceptionWithFuture(false), 'success');
});

test('異なる値', () async {
  final future1 = throwExceptionWithFuture(false);
  expect(await future1, 'success');

  final future2 =
      throwExceptionWithFuture(false).then((_) => 'Different value');
  expect(await future2, 'Different value');
});

test('await なし', () async {
  try {
    final result = throwExceptionWithFuture(true)
        .catchError((e, st) => fail('message'))
        .onError((e, st) => 'messageOnError');

    expect(await result, 'messageOnError');
  } catch (e) {
    fail('message');
  }
});

test('await なし 実験', () async {
  Future<String> func(bool willThrowException) {
    return Future.delayed(const Duration(milliseconds: 10))
        .then((_) => throwExceptionWithFuture(willThrowException));
  }

  final resultWithoutException = func(false);
  expect(await resultWithoutException, 'success');

  final resultWithException =
      func(true).onError((e, st) => 'messageOnError');
  expect(await resultWithException, 'messageOnError');
});

test('awaitなしのtry-catch (onError catchError なし)', () async {
  final completer = Completer<bool>();
  runZonedGuarded(() async {
    try {
      throwExceptionWithFuture(true);
    } catch (_) {
      fail('message');
    }
  }, (error, stack) {
    expect(error, isA<MyException>());
    completer.complete(true);
  });
  expect(completer.isCompleted, false);
  expect(await completer.future, true);
  expect(completer.isCompleted, true);
});

test('await あり', () async {
  try {
    final result = await throwExceptionWithFuture(true)
        .catchError((e, st) => fail('message'))
        .onError((e, st) => 'messageOnError');

    expect(result, 'messageOnError');
  } catch (e) {
    fail('message');
  }
});

2-2. async のつかない Future メソッド

ケース 値の取得 例外発生 適切な処理方法
通常(例外なし) 取得できる 発生しない そのまま expect() で結果を確認
await なし(catchError, onError あり) 取得できない 発生する try-catch で例外を補足
await なし(onError なし) 取得できない 発生する try-catch で例外を補足
await あり 取得できない 発生する try-catch で処理
test('通常', () async {
  expect(await throwExceptionWithFutureOnly(false), 'success');
});

test('await なし', () async {
  try {
    throwExceptionWithFutureOnly(true)
        .catchError((e, st) => fail('message'))
        .onError((e, st) => fail('message'));

    fail('message');
  } on MyException catch (_) {
    // ここを通る
  } catch (e) {
    fail('message');
  }
});

test('await, onError なし', () async {
  try {
    throwExceptionWithFutureOnly(true);
  } on MyException catch (_) {
    // ここを通る
  } catch (_) {
    fail('message');
  }
});

test('await あり', () async {
  try {
    await throwExceptionWithFutureOnly(true)
        .catchError((e, st) => fail('message'))
        .onError((e, st) => fail('message'));

    fail('message');
  } on MyException catch (_) {
    // ここを通る
  } on TestFailure catch (_) {
    rethrow;
  } catch (e) {
    fail('message');
  }
});

3. await なしで呼び出されたメソッドで例外が発生

ここから急に難しくなります、、勝手に難しく考え過ぎている気もしますが、、、

ケース 値の取得 例外発生 適切な処理方法
通常(例外なし) 取得できる 発生しない そのまま expect() で結果を確認
await なし(catchError, onError あり) 取得できるが非同期例外発生 発生する runZonedGuarded で例外を補足
await なし(onError なし) 取得できるが非同期例外発生 発生する runZonedGuarded で例外を補足
await あり 取得できない 発生する .catchError() または .onError() で処理

例外が発生しない

test('通常', () async {
  expect(await throwWithoutAwaitExceptionIn(false), 'success');
});

非同期処理内でawaitを使用してないメソッドで例外が発生した場合(catchErrorなし)

Future<String> throwExceptionWithFuture(bool willThrowException) async {
    if (willThrowException) {
      throw MyException();
    }
    return 'success';
}

Future<String> throwWithoutAwaitExceptionIn(bool willThrowException) async {
    throwExceptionWithFuture(willThrowException);
    return 'success';
}

test('await, onError なし', () async {

  // 0
  final completer = Completer<bool>();
  var isPassed = false; // ->1
  runZonedGuarded(() async {
    try {
      // 1
      final result = throwWithoutAwaitExceptionIn(true);
      expect(completer.isCompleted, false);
      expect(await result, 'success'); //->2

      // 4
      expect(completer.isCompleted, true);
      isPassed = true; // ->5
    } catch (_) {
      fail('message');
    }
  }, (ex, st) {
    // 3
    expect(ex, isA<MyException>());
    completer.complete(true);
    expect(isPassed, false); //->4
  });

  // 2
  expect(isPassed, false);
  expect(completer.isCompleted, false);
  expect(await completer.future, true); // ->3

  // 5
  expect(isPassed, true);
  expect(completer.isCompleted, true);
});

非同期処理が出てくる関係で、以下の3つの処理に分割されて、実行されます

  • テストを実行する通常の処理: 0->(停止)->2->(停止)->5
  • runZonedGuardedの非同期処理: 1->(停止)->4
  • await resultの例外処理: 3
    詳しくは、次の小項目にて。

非同期処理内でawaitを使用してないメソッドで例外が発生した場合


test('await なし', () async {
  final completer = Completer<bool>();
  int counter = 0;
  runZonedGuarded(() async {
    try {
      final result = throwWithoutAwaitExceptionIn(true)
          .catchError((e, st) => fail('message1'))
          .onError((e, st) => fail('message2'));

      expect(counter, 0);
      counter++;
      expect(counter, 1);
      expect(await result, 'success');
      expect(counter, 1);
      counter++;
      expect(counter, 2);
      // expect(completer.isCompleted, false);
    } catch (e) {
      fail('message3');
    }
  }, (ex, st) {
    expect(counter, 1);
    expect(ex, isA<MyException>());
    completer.complete(true);
    //counter++;
  });
  expect(counter, 1);
  expect(completer.isCompleted, false);

  expect(await completer.future, true);
  expect(counter, 2);
  expect(completer.isCompleted, true);
});
  • throwWithoutAwaitExceptionIn(true)await せずに呼び出しているため、即時には例外が発生せず 同期処理が続行される。非同期でないため、catchErroronErrorも実行されず、結果 expect(counter, 0);まで実行される

  • expect(await result, 'success');で非同期処理が実行され、例外処理が発生する。ここで3つの処理が実行される。同期処理のまま残りのソースを進めていく処理と、resultの非同期処理内で実行された例外処理、 runZonedGuarded(() async {の非同期処理です。

  • .catchError().onError()Future に登録される非同期エラーハンドラのため、同期的に投げられた例外は補足できません。このため、.catchError().onError() に処理を書いていても、実行されずスルーされます。非同期メソッド内でawaitがないため、try-catchでも捕捉できません

  • 例外を補足できるのは runZonedGuarded() の第2引数に渡されたゾーンエラーハンドラのみ。そのため、resultの非同期処理内で実行された例外処理は、runZonedGuarded() で実行される。

  • result自体は同期処理のため、そのまま継続される。counter は更新されるため、

    • result の await 前後は counter == 1
    • インクリメント後に counter == 2 になる
  • expect(await result, 'success');awitがあるので、runZonedGuarded(() async {の非同期処理が一時中断して、その後の expect(counter, 1); expect(completer.isCompleted, false);が実行されている。その後に、 expect(await completer.future, true);awaitがあるので、complterの完了を待つ。

  • runZonedGuarded()での例外処理。発生した時点ではexpect(counter, 1);が成立する。想定された例外が発せしており、ここを通過したことを記録するためcompleter.complete(true);を実行する。

  • completerが完了になったので、 expect(await completer.future, true);後の処理が実行される

このテストケースは、Dart の例外伝播の仕組み、非同期とawaitを使った、非常にややこしい例 になってます。非同期の順番なども関わってくる(明確に決まってないと思われる)ので、DartのバージョンやOSが変わると動作が変更される可能性もあります。
ちなみに // expect(completer.isCompleted, false);//counter++;をコメントアウトしていると、挙動が結構変わるので、非同期処理の実行の順番に最適化なども関わってきて、さらにややこしくなると思われる。

awaitを使用した呼び出したメソッドが例外が発生した場合

test('await あり', () async {
  var isPassed = false;
  //0
  final completer = Completer<bool>(); // ->1
  runZonedGuarded(() async {
    try {
      // 1
      expect(isPassed, false);
      final result = await throwWithoutAwaitExceptionIn(true)
          .catchError((e, st) => fail('message'))
          .onError((e, st) => fail('message')); // ->2

      // 4
      expect(isPassed, true);
      expect(result, 'success');
      expect(completer.isCompleted, true); // ->5
    } catch (e) {
      fail('message');
    }
  }, (ex, st) {
    // 3
    expect(ex, isA<MyException>());
    completer.complete(true); // ->4
  });

  // 2
  isPassed = true;
  expect(completer.isCompleted, false);
  expect(await completer.future, true); // ->3

  // 5
  expect(completer.isCompleted, true);
});

以下のような流れになってます

  • awaitで指定したメソッド内で例外が発生する
  • runZonedGuarded(() async {メソッドを抜けて、その続きが実行される
  • runZonedGuarded(() async {の例外処理で処理される
  • 例外が発生したawaitで指定したメソッドの続きが実行される

5が最後ですが、complterで無理矢理最後にしているだけで、基本例外処理が最後の実行になります。

4. メソッド内の非同期処理にawait あり

ケース 値の取得 例外発生 適切な処理方法
通常(例外なし) 取得できる 発生しない そのまま expect() で結果を確認
await なし(catchError, onError あり) 取得できる(catchError の値) 発生する .catchError() で例外処理
await なし(onError なし) 取得できない 発生する try-catch で例外を補足
await あり 取得できる(onError の値) 発生する try-catch または .onError() で処理
Future<String> throwExceptionWithFuture(bool willThrowException) async {
    if (willThrowException) {
      throw MyException();
    }
    return 'success';
}

Future<String> throwWithAwaitExceptionIn(bool willThrowException) async {
  await throwExceptionWithFuture(willThrowException);
  return 'success';
}

test('通常', () async {
  expect(await throwWithAwaitExceptionIn(false), 'success');
});

test('await なし', () async {
  final result = throwWithAwaitExceptionIn(true)
      .catchError((e, st) => 'catchError')
      .onError((e, st) => fail('onError'));

  expect(await result, 'catchError');
});

test('await, onError なし', () async {
  try {
    final result = throwWithAwaitExceptionIn(true);
    await result;
    fail('message');
  } on MyException catch (_) {
    // ここを通る
  } catch (_) {
    fail('message');
  }
});

test('await あり', () async {
  final result = await throwWithAwaitExceptionIn(true)
      .catchError((e, st) => fail('catchError'))
      .onError((e, st) => 'messageOnError');
  expect(result, 'messageOnError');
});

とりあえず、非同期処理をawaitで囲っておけば、onErrortry-catchで拾える。まあ、単純で初心者には良いと思われる。

方針の検討

group('戻り値がFutureでメソッドにasyncをつけてない。await せずに返す', () {
  test('onError, catchErrorでは拾えない', () async {
    expect(
        () => throwExceptionWithFutureOnly(true)
            .onError((e, st) => 'test')
            .catchError((e, st) => 'test'),
        throwsA(isA<MyException>()));
  });

  test('try-catch', () async {
    var isPassed = false;
    try {
      throwExceptionWithFutureOnly(true);
      fail('例外発生せず');
    } catch (_) {
      isPassed = true;
    }
    expect(isPassed, true);
  });

  test('run', () async {
    runZonedGuarded(() {
      throwExceptionWithFutureOnly(true);
    }, (e, st) {});
  });
});

group('戻り値がFutureでメソッドにasyncをつけて、内部でawaitを使用している', () {
  test('await なし', () async {
    final future = throwWithAwaitExceptionIn(true);
    expect(() => future, throwsA(isA<MyException>()));
  });

  test('await あり', () async {
    expect(
      () async => await throwWithAwaitExceptionIn(true),
      throwsA(isA<MyException>()),
    );
  });

  test('awaitあり→onError', () async {
    expect(
      await throwWithAwaitExceptionIn(true).onError((e, st) => 'onError'),
      'onError',
    );
  });

  test('awaitあり→onError', () async {
    final future =
        throwWithAwaitExceptionIn(true).onError((e, st) => 'onError');
    expect(await future, 'onError');
  });

  test('try-catch', () async {
    final future = throwWithAwaitExceptionIn(true)
        .onError((e, st) => throw Exception('test'));

    try {
      await future;
    } on Exception catch (ex) {
      expect(ex.toString(), 'Exception: test');
    }

    final futureWithoutOnError = throwWithAwaitExceptionIn(true);
    try {
      await futureWithoutOnError;
      fail('no reach');
    } on MyException catch (_) {}
  });

  test('runZonedGuarded', () async {
    runZonedGuarded(() async {
      await throwWithAwaitExceptionIn(true);
      fail('not reach');
    }, (e, st) {});

    runZonedGuarded(() async {
      throwWithAwaitExceptionIn(true);
      fail('not reach');
    }, (e, st) {});
  });
});
});

まとめ

  • 同期処理の例外は try-catch で処理する
  • 非同期処理 (Future) は await する場合 try-catch または .onError() を使用
  • await なしで Future を使う場合、.catchError() で処理
  • 非同期の await なしメソッドで例外が発生し、どこでもキャッチされない場合は runZonedGuarded を使用
  • 非同期の値を扱いつつ例外も処理したい場合は .catchError().onError() を適切に使用

これらの適切な処理を行うことで、Dartの非同期エラーを正しくハンドリングし、意図しないアプリのクラッシュを防ぎ、適切なハンドリングをしましょう!

ソース(テストで動作確認)

import 'dart:async';

import 'package:flutter_test/flutter_test.dart';

class MyException implements Exception {}

main() {
  String throwException(bool willThrowException) {
    if (willThrowException) {
      throw MyException();
    }
    return 'success';
  }

  Future<String> throwExceptionWithFuture(bool willThrowException) async {
    if (willThrowException) {
      throw MyException();
    }
    return 'success';
  }

  Future<String> throwExceptionWithFutureOnly(bool willThrowException) {
    if (willThrowException) {
      throw MyException();
    }
    return Future.value('success');
  }

  Future<String> throwWithoutAwaitExceptionIn(bool willThrowException) async {
    throwExceptionWithFuture(willThrowException);
    return 'success';
  }

  Future<String> throwWithAwaitExceptionIn(bool willThrowException) async {
    await throwExceptionWithFuture(willThrowException);
    return 'success';
  }

  group('同期処理', () {
    test('通常', () {
      expect(throwException(false), 'success');
    });

    test('同期処理での例外', () {
      try {
        throwException(true);
        fail('cannot reach');
      } on MyException catch (_) {
        // ここを通る
      } catch (e) {
        fail('different exception');
      }
    });
  });

  group('Futureメソッド asyncあり', () {
    test('通常', () async {
      expect(await throwExceptionWithFuture(false), 'success');
      expect(await throwExceptionWithFuture(false), 'success');
    });

    test('異なる値', () async {
      final future1 = throwExceptionWithFuture(false);
      expect(await future1, 'success');

      final future2 =
          throwExceptionWithFuture(false).then((_) => 'Different value');
      expect(await future2, 'Different value');
    });

    test('await なし', () async {
      try {
        final result = throwExceptionWithFuture(true)
            .catchError((e, st) => fail('message'))
            .onError((e, st) => 'messageOnError');

        expect(await result, 'messageOnError');
      } catch (e) {
        fail('message');
      }
    });

    test('await なし 実験', () async {
      Future<String> func(bool willThrowException) {
        return Future.delayed(const Duration(milliseconds: 10))
            .then((_) => throwExceptionWithFuture(willThrowException));
      }

      final resultWithoutException = func(false);
      expect(await resultWithoutException, 'success');

      final resultWithException =
          func(true).onError((e, st) => 'messageOnError');
      expect(await resultWithException, 'messageOnError');
    });

    test('awaitなしのtry-catch (onError catchError なし)', () async {
      final completer = Completer<bool>();
      runZonedGuarded(() async {
        try {
          throwExceptionWithFuture(true);
        } catch (_) {
          fail('message');
        }
      }, (error, stack) {
        expect(error, isA<MyException>());
        completer.complete(true);
      });
      expect(completer.isCompleted, false);
      expect(await completer.future, true);
      expect(completer.isCompleted, true);
    });

    test('await あり', () async {
      try {
        final result = await throwExceptionWithFuture(true)
            .catchError((e, st) => fail('message'))
            .onError((e, st) => 'messageOnError');

        expect(result, 'messageOnError');
      } catch (e) {
        fail('message');
      }
    });
  });

  group('Futureメソッド asyncなし', () {
    test('通常', () async {
      expect(await throwExceptionWithFutureOnly(false), 'success');
    });

    test('await なし', () async {
      try {
        throwExceptionWithFutureOnly(true)
            .catchError((e, st) => fail('message'))
            .onError((e, st) => fail('message'));

        fail('message');
      } on MyException catch (_) {
        // ここを通る
      } catch (e) {
        fail('message');
      }
    });

    test('await, onError なし', () async {
      try {
        throwExceptionWithFutureOnly(true);
      } on MyException catch (_) {
        // ここを通る
      } catch (_) {
        fail('message');
      }
    });

    test('await あり', () async {
      try {
        await throwExceptionWithFutureOnly(true)
            .catchError((e, st) => fail('message'))
            .onError((e, st) => fail('message'));

        fail('message');
      } on MyException catch (_) {
        // ここを通る
      } on TestFailure catch (_) {
        rethrow;
      } catch (e) {
        fail('message');
      }
    });
  });

  group('Futureメソッド awaitなしで呼び出されたメソッドで例外が発生', () {
    test('通常', () async {
      expect(await throwWithoutAwaitExceptionIn(false), 'success');
    });

    test('await なし', () async {
      final completer = Completer<bool>();
      int counter = 0;
      runZonedGuarded(() async {
        try {
          final result = throwWithoutAwaitExceptionIn(true)
              .catchError((e, st) => fail('message1'))
              .onError((e, st) => fail('message2'));

          expect(counter, 0);
          counter++;
          expect(counter, 1);
          expect(await result, 'success');
          expect(counter, 1);
          counter++;
          expect(counter, 2);
          // expect(completer.isCompleted, false);
        } catch (e) {
          fail('message3');
        }
      }, (ex, st) {
        expect(counter, 1);
        expect(ex, isA<MyException>());
        completer.complete(true);
        //counter++;
      });
      expect(counter, 1);
      expect(completer.isCompleted, false);

      expect(await completer.future, true);
      expect(counter, 2);
      expect(completer.isCompleted, true);
    });

    test('await, onError なし', () async {
      // 0
      final completer = Completer<bool>();
      var isPassed = false; // ->1
      runZonedGuarded(() async {
        try {
          // 1
          final result = throwWithoutAwaitExceptionIn(true);
          expect(completer.isCompleted, false);
          expect(await result, 'success'); //->2

          // 4
          expect(completer.isCompleted, true);
          isPassed = true; // ->5
        } catch (_) {
          fail('message');
        }
      }, (ex, st) {
        // 3
        expect(ex, isA<MyException>());
        completer.complete(true);
        expect(isPassed, false); //->4
      });

      // 2
      expect(isPassed, false);
      expect(completer.isCompleted, false);
      expect(await completer.future, true); // ->3

      // 5
      expect(isPassed, true);
      expect(completer.isCompleted, true);
    });

    test('await あり', () async {
      var isPassed = false;
      //0
      final completer = Completer<bool>(); // ->1
      runZonedGuarded(() async {
        try {
          // 1
          expect(isPassed, false);
          final result = await throwWithoutAwaitExceptionIn(true)
              .catchError((e, st) => fail('message'))
              .onError((e, st) => fail('message')); // ->2

          // 4
          expect(isPassed, true);
          expect(result, 'success');
          expect(completer.isCompleted, true); // ->5
        } catch (e) {
          fail('message');
        }
      }, (ex, st) {
        // 3
        expect(ex, isA<MyException>());
        completer.complete(true); // ->4
      });

      // 2
      isPassed = true;
      expect(completer.isCompleted, false);
      expect(await completer.future, true); // ->3

      // 5
      expect(completer.isCompleted, true);
    });
  });

  group('Futureメソッド awaitありで呼び出されたメソッドで例外が発生', () {
    test('通常', () async {
      expect(await throwWithAwaitExceptionIn(false), 'success');
    });

    test('await なし', () async {
      final result = throwWithAwaitExceptionIn(true)
          .catchError((e, st) => 'catchError')
          .onError((e, st) => fail('onError'));

      expect(await result, 'catchError');
    });

    test('await, onError なし', () async {
      try {
        final result = throwWithAwaitExceptionIn(true);
        await result;
        fail('message');
      } on MyException catch (_) {
        // ここを通る
      } catch (_) {
        fail('message');
      }
    });

    test('await あり', () async {
      final result = await throwWithAwaitExceptionIn(true)
          .catchError((e, st) => fail('catchError'))
          .onError((e, st) => 'messageOnError');
      expect(result, 'messageOnError');
    });
  });

  group('対応', () {
    test('非同期処理の例外があっても無視する', () {
      void testFunction(bool willThrowException) {
        throwExceptionWithFuture(willThrowException).onError((_, __) {
          return '';
        });
      }

      testFunction(false);
      testFunction(true);
    });

    test('非同期処理の例外に対応する', () async {
      String? errorMessage;
      Future<void> testFunction(bool willThrowException) async {
        errorMessage = '';
        throwExceptionWithFuture(willThrowException).onError((_, __) {
          errorMessage = 'error';
          return '';
        });
      }

      expect(errorMessage, null);

      testFunction(false);
      await Future.delayed(const Duration(milliseconds: 10));
      expect(errorMessage, '');

      testFunction(true);
      await Future.delayed(const Duration(milliseconds: 10));
      expectLater(errorMessage, 'error');
    });
  });

  group('方針の検討', () {
    group('戻り値がFutureでメソッドにasyncをつけてない。await せずに返す', () {
      test('onError, catchErrorでは拾えない', () async {
        expect(
            () => throwExceptionWithFutureOnly(true)
                .onError((e, st) => 'test')
                .catchError((e, st) => 'test'),
            throwsA(isA<MyException>()));
      });

      test('try-catch', () async {
        var isPassed = false;
        try {
          throwExceptionWithFutureOnly(true);
          fail('例外発生せず');
        } catch (_) {
          isPassed = true;
        }
        expect(isPassed, true);
      });

      test('run', () async {
        runZonedGuarded(() {
          throwExceptionWithFutureOnly(true);
        }, (e, st) {});
      });
    });

    group('戻り値がFutureでメソッドにasyncをつけて、内部でawaitを使用している', () {
      test('await なし', () async {
        final future = throwWithAwaitExceptionIn(true);
        expect(() => future, throwsA(isA<MyException>()));
      });

      test('await あり', () async {
        expect(
          () async => await throwWithAwaitExceptionIn(true),
          throwsA(isA<MyException>()),
        );
      });

      test('awaitあり→onError', () async {
        expect(
          await throwWithAwaitExceptionIn(true).onError((e, st) => 'onError'),
          'onError',
        );
      });

      test('awaitあり→onError', () async {
        final future =
            throwWithAwaitExceptionIn(true).onError((e, st) => 'onError');
        expect(await future, 'onError');
      });

      test('try-catch', () async {
        final future = throwWithAwaitExceptionIn(true)
            .onError((e, st) => throw Exception('test'));

        try {
          await future;
        } on Exception catch (ex) {
          expect(ex.toString(), 'Exception: test');
        }

        final futureWithoutOnError = throwWithAwaitExceptionIn(true);
        try {
          await futureWithoutOnError;
          fail('no reach');
        } on MyException catch (_) {}
      });

      test('runZonedGuarded', () async {
        runZonedGuarded(() async {
          await throwWithAwaitExceptionIn(true);
          fail('not reach');
        }, (e, st) {});

        runZonedGuarded(() async {
          throwWithAwaitExceptionIn(true);
          fail('not reach');
        }, (e, st) {});
      });
    });
  });
}