目次

Futureとエラー処理

Dart言語はネイティブの非同期サポートを備えているため、非同期Dartコードの読み書きがはるかに容易になります。ただし、一部のコード(特に古いコード)では、then()catchError()whenComplete()などのFutureメソッドが依然として使用されている場合があります。

このページは、これらのFutureメソッドを使用する際の一般的な落とし穴を回避するのに役立ちます。

Future APIとコールバック

#

Future APIを使用する関数は、Futureを完了させる値(またはエラー)を処理するコールバックを登録します。例えば

dart
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()のエラーハンドラとしての汎用性を示しています。

dart
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()内でのエラー処理

#

より詳細なエラー処理を行うには、エラーで完了したFutureを処理するために、2番目の(onError)コールバックをthen()内に登録できます。これがthen()のシグネチャです。

dart
Future<R> then<R>(FutureOr<R> Function(T value) onValue, {Function? onError});

to then()に転送されたエラーと、then()で生成されたエラーを区別したい場合にのみ、オプションのonErrorコールバックを登録してください。

dart
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()を使用してチェーンの任意の部分から生成されたエラーをキャッチすることが一般的です。

dart
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を取ります。

dart
Future<T> catchError(Function onError, {bool Function(Object error)? test});

handleAuthResponse(params)は、提供されたパラメータに基づいてユーザーを認証し、ユーザーを適切なURLにリダイレクトする関数とします。複雑なワークフローを考えると、handleAuthResponse()はさまざまなエラーと例外を生成する可能性があり、それらを異なる方法で処理する必要があります。testを使用してそれを行う方法を以下に示します。

dart
void main() {
  handleAuthResponse(const {'username': 'dash', 'age': 3})
      .then((_) => ...)
      .catchError(handleFormatException, test: (e) => e is FormatException)
      .catchError(handleAuthorizationException,
          test: (e) => e is AuthorizationException);
}

whenComplete()を使用した非同期try-catch-finally

#

then().catchError()がtry-catchを反映している場合、whenComplete()は「finally」に相当します。whenComplete()内で登録されたコールバックは、whenComplete()の受信者が値で完了する場合でもエラーで完了する場合でも、完了時に呼び出されます。

dart
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もそのエラーで完了します。

dart
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がエラーで完了しますが、これはcatchError()によって処理されます。catchError()のFutureがsomeObjectで完了するため、whenComplete()のFutureもその同じオブジェクトで完了します。

dart
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はそのエラーで完了します。

dart
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がエラーで完了し、エラーハンドラがまだアタッチされておらず、エラーが誤って伝播するというシナリオを回避します。以下のコードを考えてください。

dart
void main() {
  Future<Object> future = asyncErrorFunction();

  // BAD: Too late to handle asyncErrorFunction() exception.
  Future.delayed(const Duration(milliseconds: 500), () {
    future.then(...).catchError(...);
  });
}

上記のコードでは、asyncErrorFunction()が呼び出されてから0.5秒後にcatchError()が登録されるため、エラーは未処理になります。

asyncErrorFunction()Future.delayed()のコールバック内で呼び出すと、問題は解決します。

dart
void main() {
  Future.delayed(const Duration(milliseconds: 500), () {
    asyncErrorFunction()
        .then(...)
        .catchError(...); // We get here.
  });
}

潜在的な問題:同期エラーと非同期エラーを誤って混在させること

#

Futureを返す関数は、ほぼ常に将来そのエラーを発生させるべきです。そのような関数の呼び出し元が複数のエラー処理シナリオを実装する必要がないように、同期エラーが漏洩するのを防ぎたいと考えています。以下のコードを考えてください。

dart
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()コールバック内で呼び出されません。これがスローした場合、同期エラーが伝播します。

dart
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()コールバックでラップすることです。

dart
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()ですべてのエラーを処理できます。

dart
void main() {
  parseAndRead(data).catchError((e) {
    print('Inside catchError');
    print(e);
    return -1;
  });
}

// Program Output:
//   Inside catchError
//   <error from obtainFilename>

Future.sync()は、キャッチされない例外に対するコードの堅牢性を高めます。関数に多くのコードが詰め込まれている場合、危険なことをしているのに気づかない可能性があります。

dart
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リファレンスを参照してください。