Future 和錯誤處理
- Future API 和回撥
- 使用 then() 和 catchError() 的示例
- 使用 whenComplete() 的非同步 try-catch-finally
- 潛在問題:未能及早註冊錯誤處理程式
- 潛在問題:意外混合同步和非同步錯誤
- 更多資訊
Dart 語言原生支援非同步,這使得非同步 Dart 程式碼更易於閱讀和編寫。然而,一些程式碼——特別是舊程式碼——可能仍在使用 Future 方法,例如 then()、catchError() 和 whenComplete()。
本頁面可以幫助您避免在使用這些 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() 中註冊第二個(onError)回撥來處理以錯誤完成的 Future。以下是 then() 的簽名
Future<R> then<R>(FutureOr<R> Function(T value) onValue, {Function? onError});僅當您想區分轉發給 then() 的錯誤和 then() 內部生成的錯誤時,才註冊可選的 onError 回撥
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() 捕獲鏈中任何部分生成的錯誤
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() 的非同步 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 以錯誤完成,該錯誤現在由 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() 呼叫半秒後才註冊,導致錯誤未被處理。
如果 asyncErrorFunction() 在 Future.delayed() 回撥中呼叫,問題就會消失
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.
});
}該程式碼中有兩個函式可能會同步丟擲異常: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 參考。