目录

Futures 和错误处理

Dart 语言原生支持 异步 ,使得异步 Dart 代码更易于阅读和编写。但是,一些代码——特别是较旧的代码——可能仍然使用 Future 方法 ,例如 then()catchError()whenComplete()

本页面可以帮助您避免使用这些 Future 方法时的一些常见陷阱。

Future API 和回调函数

#

使用 Future API 的函数注册回调函数来处理完成 Future 的值(或错误)。例如:

dart
myFunc().then(processValue).catchError(handleError);

注册的回调函数根据以下规则触发:如果在使用值为完成的 Future 上调用 then() ,则其回调函数会触发;如果在使用错误值为完成的 Future 上调用 catchError() ,则其回调函数会触发。

在上面的示例中,如果 myFunc() 的 Future 使用值为完成,则 then() 的回调函数会触发。如果在 then() 内没有产生新的错误,则 catchError() 的回调函数不会触发。另一方面,如果 myFunc() 使用错误值为完成,则 then() 的回调函数不会触发,而 catchError() 的回调函数会触发。

使用 then() 和 catchError() 的示例

#

链式 then()catchError() 调用在处理 Futures 时是一种常见模式,可以将其视为 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() 内的错误处理

#

为了更精细的错误处理,您可以在 then() 内注册第二个 (onError) 回调函数来处理使用错误值为完成的 Futures。以下是 then() 的签名:

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

只有当您想区分转发 then() 的错误和在 then() 生成的错误时,才注册可选的 onError 回调函数:

dart
asyncErrorFunction().then(successCallback, onError: (e) {
  handleError(e); // 原始错误。
  anotherAsyncErrorFunction(); // 哎呀,新的错误。
}).catchError(handleError); // 处理 then() 内的错误。

在上面的示例中, asyncErrorFunction() 的 Future 的错误由 onError 回调函数处理; anotherAsyncErrorFunction() 导致 then() 的 Future 使用错误值为完成;此错误由 catchError() 处理。

一般来说,不推荐实现两种不同的错误处理策略:只有在必须在 then() 内捕获错误时,才注册第二个回调函数。

长链中间的错误

#

通常会有连续的 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 使用 "from one" 完成。
      .then((_) => two()) // Future 使用 two() 的错误完成。
      .then((_) => three()) // Future 使用 two() 的错误完成。
      .then((_) => four()) // Future 使用 two() 的错误完成。
      .then((value) => value.length) // Future 使用 two() 的错误完成。
      .catchError((e) {
    print('Got error: $e'); // 最后,回调函数触发。
    return 42; // Future 使用 42 完成。
  }).then((value) {
    print('The value is $value');
  });
}

// 此程序的输出:
//   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() 实现 Async 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 使用错误完成:
      .then((_) => print("Won't reach here"))
      // Future 使用相同的错误完成:
      .whenComplete(() => print('Reaches here'))
      // Future 使用相同的错误完成:
      .then((_) => print("Won't reach here"))
      // 此处处理错误:
      .catchError(handleError);
}

在下面的代码中, then() 的 Future 使用错误值为完成,该错误现在由 catchError() 处理。因为 catchError() 的 Future 使用 someObject 完成,所以 whenComplete() 的 Future 也使用相同的对象完成。

dart
void main() {
  asyncErrorFunction()
      // Future 使用错误完成:
      .then((_) => ...)
      .catchError((e) {
    handleError(e);
    printErrorMessage();
    return someObject; // Future 使用 someObject 完成
  }).whenComplete(() => print('Done!')); // Future 使用 someObject 完成
}

来自 whenComplete() 的错误

#

如果 whenComplete() 的回调函数抛出错误,则 whenComplete() 的 Future 使用该错误值为完成:

dart
void main() {
  asyncErrorFunction()
      // Future 使用值为完成:
      .catchError(handleError)
      // Future 使用错误完成:
      .whenComplete(() => throw Exception('New error'))
      // 处理错误:
      .catchError(handleError);
}

潜在问题:未能尽早注册错误处理程序

#

至关重要的是,错误处理程序必须在 Future 完成之前安装:这避免了 Future 使用错误值为完成、错误处理程序尚未附加以及错误意外传播的情况。考虑以下代码:

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

  // 错误:为时已晚,无法处理 asyncErrorFunction() 异常。
  Future.delayed(const Duration(milliseconds: 500), () {
    future.then(...).catchError(...);
  });
}

在上面的代码中, catchError() 直到调用 asyncErrorFunction() 半秒钟后才注册,并且错误未被处理。

如果在 Future.delayed() 回调函数内调用 asyncErrorFunction() ,则问题就会消失:

dart
void main() {
  Future.delayed(const Duration(milliseconds: 500), () {
    asyncErrorFunction()
        .then(...)
        .catchError(...); // 我们到达这里。
  });
}

潜在问题:意外混合同步和异步错误

#

返回 Futures 的函数几乎总是应该在未来发出它们的错误。由于我们不希望此类函数的调用者必须实现多种错误处理方案,因此我们希望防止任何同步错误泄漏。考虑以下代码:

dart
Future<int> parseAndRead(Map<String, dynamic> data) {
  final filename = obtainFilename(data); // 可能抛出异常。
  final file = File(filename);
  return file.readAsString().then((contents) {
    return parseFileData(contents); // 可能抛出异常。
  });
}

该代码中的两个函数可能同步抛出异常: 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;
  });
}

// 程序输出:
//   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); // 可能抛出异常。
    final file = File(filename);
    return file.readAsString().then((contents) {
      return parseFileData(contents); // 可能抛出异常。
    });
  });
}

如果回调函数返回一个非 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;
  });
}

// 程序输出:
//   Inside catchError
//   <error from obtainFilename>

Future.sync() 使您的代码能够抵御未捕获的异常。如果您的函数包含大量代码,则您可能会在未意识到的情况下执行某些危险操作:

dart
Future fragileFunc() {
  return Future.sync(() {
    final x = someFunc(); // 在某些罕见情况下意外抛出异常。
    var y = 10 / x; // x 不应等于 0。
    ...
  });
}

Future.sync() 不仅允许您处理已知可能发生的错误,而且还可以防止错误 意外地 从您的函数中泄漏。

更多信息

#

有关 Futures 的更多信息,请参见 Future API 参考