跳到主要內容

Futures 與錯誤處理

Dart 語言具有原生非同步支援,使非同步 Dart 程式碼更易於讀寫。然而,某些程式碼(尤其是較舊的程式碼)可能仍使用 Future 方法,例如 then()catchError()whenComplete()

本頁面可以幫助您避免在使用這些 Future 方法時的一些常見陷阱。

Future API 與回呼

#

使用 Future API 的函式註冊回呼,以處理完成 Future 的值(或錯誤)。例如

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

註冊的回呼根據以下規則觸發:如果 then() 在以值完成的 Future 上調用,則 then() 的回呼會觸發;如果 catchError() 在以錯誤完成的 Future 上調用,則 catchError() 的回呼會觸發。

在上面的範例中,如果 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() 內的錯誤處理

#

對於更精細的錯誤處理,您可以在 then() 內註冊第二個 (onError) 回呼,以處理以錯誤完成的 Future。以下是 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); // Original error.
        anotherAsyncErrorFunction(); // Oops, new error.
      },
    )
    .catchError(handleError); // Error from within then() handled.

在上面的範例中,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 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() 半秒後才註冊 catchError(),並且錯誤未經處理。

如果在 Future.delayed() 回呼中調用 asyncErrorFunction(),則問題會消失

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

潛在問題:意外混合同步和非同步錯誤

#

傳回 Future 的函式幾乎總是應該在 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.
  });
}

該程式碼中的兩個函式可能會同步拋出錯誤: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 API 參考以取得有關 Future 的更多資訊。