Future とエラー処理
- Future API とコールバック
- then() と catchError() を使用する例
- whenComplete() を使用した async try-catch-finally
- 潜在的な問題: エラーハンドラーの登録が遅れる
- 潜在的な問題: 同期エラーと非同期エラーを誤って混在させる
- 詳細情報
Dart 言語にはネイティブな 非同期サポート があり、非同期 Dart コードの読み書きがずっと容易になります。しかし、一部のコード(特に古いコード)では、then()、catchError()、whenComplete() などの Future メソッド が依然として使用されている場合があります。
このページは、これらの Future メソッドを使用する際に発生しがちな一般的な落とし穴を回避するのに役立ちます。
Future API とコールバック
#Future API を使用する関数は、Future を完了させる値(またはエラー)を処理するコールバックを登録します。たとえば、
myFunc().then(processValue).catchError(handleError);登録されたコールバックは、次のルールに基づいて実行されます。then() のコールバックは、値で完了する Future で呼び出された場合に実行されます。catchError() のコールバックは、エラーで完了する Future で呼び出された場合に実行されます。
上記の例では、myFunc() の Future が値で完了した場合、then() のコールバックが実行されます。then() 内で新しいエラーが生成されなかった場合、catchError() のコールバックは実行されません。一方、myFunc() がエラーで完了した場合、then() のコールバックは実行されず、catchError() のコールバックが実行されます。
then() と catchError() を使用する例
#then() と catchError() の連鎖呼び出しは、Future を扱う際の一般的なパターンであり、try-catch ブロックのほぼ同等物と考えることができます。
次のセクションでは、このパターンの例を示します。
包括的なエラーハンドラーとしての catchError()
#次の例では、then() のコールバック内から例外をスローするケースを扱い、エラーハンドラーとしての catchError() の汎用性を示します。
myFunc()
    .then((value) {
      doSomethingWith(value);
      ...
      throw Exception('Some arbitrary error');
    })
    .catchError(handleError);myFunc() の Future が値で完了した場合、then() のコールバックが実行されます。then() のコールバック内のコードがスローされた場合(上記の例のように)、then() の Future はエラーで完了します。そのエラーは catchError() によって処理されます。
myFunc() の Future がエラーで完了した場合、then() の Future はそのエラーで完了します。そのエラーも catchError() によって処理されます。
エラーが myFunc() 内で発生したか、then() 内で発生したかに関わらず、catchError() はそれを正常に処理します。
then() 内でのエラー処理
#より詳細なエラー処理を行うには、then() 内に 2 番目の(onError)コールバックを登録して、エラーで完了した Future を処理できます。以下は then() のシグネチャです。
Future<R> then<R>(FutureOr<R> Function(T value) onValue, {Function? onError});オプションの onError コールバックは、then() に転送されたエラーと、then() 内で生成されたエラーを区別したい場合にのみ登録してください。
asyncErrorFunction()
    .then(
      successCallback,
      onError: (e) {
        handleError(e); // Original error.
        anotherAsyncErrorFunction(); // Oops, new error.
      },
    )
    .catchError(handleError); // Error from within then() handled.上記の例では、asyncErrorFunction() の Future のエラーは onError コールバックで処理されます。anotherAsyncErrorFunction() は then() の Future がエラーで完了する原因となり、このエラーは catchError() によって処理されます。
一般的に、2 つの異なるエラー処理戦略を実装することは推奨されません。then() 内でエラーをキャッチする正当な理由がある場合にのみ、2 番目のコールバックを登録してください。
長いチェーンの途中で発生するエラー
#一連の then() 呼び出しを行い、チェーンのどの部分で発生したエラーでも catchError() を使用してキャッチすることは一般的です。
Future<String> one() => Future.value('from one');
Future<String> two() => Future.error('error from two');
Future<String> three() => Future.value('from three');
Future<String> four() => Future.value('from four');
void main() {
  one() // Future completes with "from one".
      .then((_) => two()) // Future completes with two()'s error.
      .then((_) => three()) // Future completes with two()'s error.
      .then((_) => four()) // Future completes with two()'s error.
      .then((value) => value.length) // Future completes with two()'s error.
      .catchError((e) {
        print('Got error: $e'); // Finally, callback fires.
        return 42; // Future completes with 42.
      })
      .then((value) {
        print('The value is $value');
      });
}
// Output of this program:
//   Got error: error from two
//   The value is 42上記のコードでは、one() の Future は値で完了しますが、two() の Future はエラーで完了します。エラーで完了した Future で then() が呼び出されると、then() のコールバックは実行されません。代わりに、then() の Future はそのレシーバーのエラーで完了します。この例では、two() が呼び出された後、後続のすべての then() が返す Future は two() のエラーで完了することを意味します。そのエラーは最終的に catchError() で処理されます。
特定のエラーを処理する
#特定のエラーをキャッチしたい場合はどうでしょうか? あるいは、複数のエラーをキャッチしたい場合は?
catchError() はオプションの名前付き引数 test を受け取ります。これにより、スローされたエラーの種類を照会できます。
Future<T> catchError(Function onError, {bool Function(Object error)? test});handleAuthResponse(params) を考えてみましょう。これは、提供されたパラメータに基づいてユーザーを認証し、適切な URL にリダイレクトする関数です。複雑なワークフローを考慮すると、handleAuthResponse() はさまざまなエラーや例外を生成する可能性があり、それらを個別に処理する必要があります。test を使用してこれを実現する方法を以下に示します。
void main() {
  handleAuthResponse(const {'username': 'dash', 'age': 3})
      .then((_) => ...)
      .catchError(handleFormatException, test: (e) => e is FormatException)
      .catchError(
        handleAuthorizationException,
        test: (e) => e is AuthorizationException,
      );
}whenComplete() を使用した async try-catch-finally
#then().catchError() が try-catch に相当するのに対し、whenComplete() は 'finally' に相当します。whenComplete() 内に登録されたコールバックは、whenComplete() のレシーバーが値またはエラーで完了したときに呼び出されます。
final server = connectToServer();
server
    .post(myUrl, fields: const {'name': 'Dash', 'profession': 'mascot'})
    .then(handleResponse)
    .catchError(handleError)
    .whenComplete(server.close);server.post() が有効な応答またはエラーを生成したかどうかにかかわらず、server.close を呼び出したいと考えています。これは whenComplete() の中に配置することで保証されます。
whenComplete() が返す Future を完了させる
#whenComplete() 内からエラーが発行されない場合、その Future は whenComplete() が呼び出された Future と同じように完了します。これは例で理解するのが最も簡単です。
以下のコードでは、then() の Future はエラーで完了するため、whenComplete() の Future もそのエラーで完了します。
void main() {
  asyncErrorFunction()
      // Future completes with an error:
      .then((_) => print("Won't reach here"))
      // Future completes with the same error:
      .whenComplete(() => print('Reaches here'))
      // Future completes with the same error:
      .then((_) => print("Won't reach here"))
      // Error is handled here:
      .catchError(handleError);
}以下のコードでは、then() の Future はエラーで完了し、これは now catchError() によって処理されます。catchError() の Future が someObject で完了するため、whenComplete() の Future も同じオブジェクトで完了します。
void main() {
  asyncErrorFunction()
      // Future completes with an error:
      .then((_) => ...)
      .catchError((e) {
        handleError(e);
        printErrorMessage();
        return someObject; // Future completes with someObject
      })
      .whenComplete(() => print('Done!')); // Future completes with someObject
}whenComplete() 内で発生したエラー
#whenComplete() のコールバックがエラーをスローした場合、whenComplete() の Future はそのエラーで完了します。
void main() {
  asyncErrorFunction()
      // Future completes with a value:
      .catchError(handleError)
      // Future completes with an error:
      .whenComplete(() => throw Exception('New error'))
      // Error is handled:
      .catchError(handleError);
}潜在的な問題: エラーハンドラーの登録が遅れる
#エラーハンドラーが Future が完了する前にインストールされていることが重要です。これにより、Future がエラーで完了し、エラーハンドラーがまだアタッチされておらず、エラーが誤って伝播するというシナリオを回避できます。このコードを検討してください。
void main() {
  Future<Object> future = asyncErrorFunction();
  // BAD: Too late to handle asyncErrorFunction() exception.
  Future.delayed(const Duration(milliseconds: 500), () {
    future.then(...).catchError(...);
  });
}上記のコードでは、catchError() は asyncErrorFunction() が呼び出されてから半秒後に登録されるため、エラーは処理されません。
Future.delayed() コールバック内で asyncErrorFunction() が呼び出された場合、問題は解消されます。
void main() {
  Future.delayed(const Duration(milliseconds: 500), () {
    asyncErrorFunction()
        .then(...)
        .catchError(...); // We get here.
  });
}潜在的な問題: 同期エラーと非同期エラーを誤って混在させる
#Future を返す関数は、ほぼ常に Future 内でエラーを発行すべきです。このような関数の呼び出し元に複数のエラー処理シナリオを実装させたくないため、同期エラーが漏れ出さないようにしたいと考えています。このコードを検討してください。
Future<int> parseAndRead(Map<String, dynamic> data) {
  final filename = obtainFilename(data); // Could throw.
  final file = File(filename);
  return file.readAsString().then((contents) {
    return parseFileData(contents); // Could throw.
  });
}そのコードの 2 つの関数、obtainFilename() と parseFileData() は同期的にスローされる可能性があります。parseFileData() は then() コールバック内で実行されるため、そのエラーは関数の外部に漏れ出しません。代わりに、then() の Future は parseFileData() のエラーで完了し、エラーは最終的に parseAndRead() の Future を完了させ、エラーは catchError() によって正常に処理できます。
しかし、obtainFilename() は then() コールバック内で呼び出されていません。それが スローされた場合、同期エラーが伝播します。
void main() {
  parseAndRead(data).catchError((e) {
    print('Inside catchError');
    print(e);
    return -1;
  });
}
// Program Output:
//   Unhandled exception:
//   <error from obtainFilename>
//   ...catchError() を使用してもエラーがキャプチャされないため、parseAndRead() のクライアントは、このエラーに対して個別のエラー処理戦略を実装することになります。
解決策: Future.sync() を使用してコードをラップする
#関数から同期エラーが誤ってスローされないようにする一般的なパターンは、関数本体を新しい Future.sync() コールバックでラップすることです。
Future<int> parseAndRead(Map<String, dynamic> data) {
  return Future.sync(() {
    final filename = obtainFilename(data); // Could throw.
    final file = File(filename);
    return file.readAsString().then((contents) {
      return parseFileData(contents); // Could throw.
    });
  });
}コールバックが非 Future 値を返す場合、Future.sync() の Future はその値で完了します。コールバックがスローされた場合(上記の例のように)、Future はエラーで完了します。コールバック自体が Future を返す場合、その Future の値またはエラーが Future.sync() の Future を完了させます。
Future.sync() でラップされたコードを使用すると、catchError() はすべて のエラーを処理できます。
void main() {
  parseAndRead(data).catchError((e) {
    print('Inside catchError');
    print(e);
    return -1;
  });
}
// Program Output:
//   Inside catchError
//   <error from obtainFilename>Future.sync() は、未処理の例外に対してコードを回復力のあるものにします。関数に多くのコードが詰め込まれている場合、気づかずに危険なことをしている可能性があります。
Future fragileFunc() {
  return Future.sync(() {
    final x = someFunc(); // Unexpectedly throws in some rare cases.
    var y = 10 / x; // x should not equal 0.
    ...
  });
}Future.sync() は、発生する可能性がわかっているエラーを処理できるだけでなく、エラーが関数から誤って漏れ出るのを防ぎます。
詳細情報
#Future に関する詳細については、Future API リファレンス を参照してください。