Update `Future.onError` extension method.
Now works better with futures that have been up-cast.
CoreLibraryReviewExempt: Rewrite of existing pure Dart function.
Change-Id: Iefd05b9cfd8ff0cc3e27fc1122a670030e0901ff
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/283680
Reviewed-by: Nate Bosch <nbosch@google.com>
Commit-Queue: Lasse Nielsen <lrn@google.com>
diff --git a/sdk/lib/async/future.dart b/sdk/lib/async/future.dart
index 65155ef..dc002cc 100644
--- a/sdk/lib/async/future.dart
+++ b/sdk/lib/async/future.dart
@@ -748,8 +748,11 @@
/// and a stack trace for the error.
/// In the case of `onError`,
/// if the exception thrown is `identical` to the error argument to `onError`,
+ /// and it is thrown *synchronously*
/// the throw is considered a rethrow,
/// and the original stack trace is used instead.
+ /// To rethrow with the same stack trace in an asynchronous callback,
+ /// use [Error.throwWithStackTrace].
///
/// If the callback returns a [Future],
/// the future returned by `then` will be completed with
@@ -1054,12 +1057,20 @@
Future<T> onError<E extends Object>(
FutureOr<T> handleError(E error, StackTrace stackTrace),
{bool test(E error)?}) {
- // There are various ways to optimize this to avoid the double is E/as E
- // type check, but for now we are not optimizing the error path.
- return this.catchError(
- (Object error, StackTrace stackTrace) =>
- handleError(error as E, stackTrace),
- test: (Object error) => error is E && (test == null || test(error)));
+ FutureOr<T> onError(Object error, StackTrace stackTrace) {
+ if (error is! E || test != null && !test(error)) {
+ // Counts as rethrow, preserves stack trace.
+ throw error;
+ }
+ return handleError(error, stackTrace);
+ }
+ if (this is _Future<Object?>) {
+ // Internal method working like `catchError`,
+ // but allows specifying a different result future type.
+ return unsafeCast<_Future<T>>(this)._safeOnError<T>(onError);
+ }
+
+ return this.then<T>((T value) => value, onError: onError);
}
/// Completely ignores this future and its result.
@@ -1110,7 +1121,7 @@
/// A way to produce Future objects and to complete them later
/// with a value or error.
///
-/// Most of the time, the simplest way to create a future is to just use
+/// Most of the time, the simples t way to create a future is to just use
/// one of the [Future] constructors to capture the result of a single
/// asynchronous computation:
/// ```dart
diff --git a/sdk/lib/async/future_impl.dart b/sdk/lib/async/future_impl.dart
index f8fb750..812fff4 100644
--- a/sdk/lib/async/future_impl.dart
+++ b/sdk/lib/async/future_impl.dart
@@ -349,6 +349,23 @@
return result;
}
+ // Used by extension method `onError` to up-cast the result to `R`.
+ //
+ // Not public because we cannot statically ensure that [R] is a supertype
+ // of [T].
+ //
+ // Avoids needing to allocate an extra value handler to do the up cast,
+ // which is needed if using `.then`.
+ Future<R> _safeOnError<R>(FutureOr<R> Function(Object, StackTrace) onError) {
+ assert(this is _Future<R>); // Is up-cast.
+ _Future<R> result = _Future<R>();
+ if (!identical(result._zone, _rootZone)) {
+ onError = result._zone.registerBinaryCallback(onError);
+ }
+ _addListener(_FutureListener<T, R>.catchError(result, onError, null));
+ return result;
+ }
+
Future<T> whenComplete(dynamic action()) {
_Future<T> result = new _Future<T>();
if (!identical(result._zone, _rootZone)) {
diff --git a/tests/lib/async/future_onerror_test.dart b/tests/lib/async/future_onerror_test.dart
index ea5ffab..307f76e 100644
--- a/tests/lib/async/future_onerror_test.dart
+++ b/tests/lib/async/future_onerror_test.dart
@@ -2,114 +2,213 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-import 'package:async_helper/async_helper.dart';
+import "dart:async";
+
+import "package:async_helper/async_helper.dart";
import "package:expect/expect.dart";
main() {
+ // Values used.
+ var error = StateError("test");
var stack = StackTrace.fromString("for testing");
- var error = Object();
- var stateError = StateError("test");
+ var expectedError = AsyncError(error, stack);
+
+ var otherError = ArgumentError("Other error");
+ var expectedOtherError = AsyncError(otherError, StackTrace.empty);
+
+ var errorFuture = Future<int?>.error(error, stack)..ignore();
+ var nonNativeErrorFuture = NonNativeFuture<int?>._(errorFuture);
asyncStart();
- {
- // Match any error.
+ /// Perform `onError` on [future] with arguments, and expect [result].
+ ///
+ /// If [result] is [AsyncError], expecte an error result with
+ /// that error and stack trace.
+ /// If [result.stackTrace] is [StackTrace.empty],
+ /// check that the stack trace is *not* the original [stack].
+ ///
+ /// Otherwise expect the future to complete with value equal to [result].
+ void test<E extends Object, T>(
+ String testName,
+ Future<T> future,
+ FutureOr<T> Function(E, StackTrace) onError, {
+ bool Function(E)? test,
+ Object? result,
+ }) {
asyncStart();
- var future = Future.error(error, stack);
- future.onError((e, s) {
- Expect.identical(error, e);
- Expect.identical(stack, s);
- asyncEnd();
- });
+ var resultFuture = future.onError(onError, test: test);
+
+ if (result is AsyncError) {
+ resultFuture.then((value) {
+ Expect.fail("$testName: Did not throw, value: $value");
+ }, onError: (Object error, StackTrace stackTrace) {
+ Expect.identical(result.error, error, testName);
+ if (result.stackTrace != StackTrace.empty) {
+ Expect.identical(result.stackTrace, stackTrace, testName);
+ } else {
+ Expect.notEquals(stack, stackTrace, testName);
+ }
+ asyncEnd();
+ });
+ } else {
+ resultFuture.then((value) {
+ Expect.equals(result, value);
+ asyncEnd();
+ }, onError: (error, stackTrace) {
+ Expect.fail("$testName: Threw $error");
+ });
+ }
}
- {
- // With matching test.
- asyncStart();
- var future = Future.error(error, stack);
- future.onError((e, s) {
- Expect.identical(error, e);
- Expect.identical(stack, s);
- asyncEnd();
- }, test: (o) => true);
+ // Go through all the valid permutations of the following options:
+ //
+ // * Native future or non-native future.
+ // * The error type parameter matches the error or not.
+ // * If type matches, test function is provided or not.
+ // * If test function provided, it returns either true or false.
+ // * If type and test both succeeds,
+ // * Error handler is synchronous or asynchronous
+ // * Error handler returns a value, throws new error,
+ // or rethrows same error.
+ // * The future is up-cast to a supertype or not when `onError` is called.
+ // (Since `onError` is an extension method, it affects the method.)
+ //
+ // The expected result is computed for each combination,
+ // and checked against the behavior:
+ // * If type or test fails to match,
+ // the original error and stack is the error result
+ // * If type matches and test doesn't reject,
+ // the error handler is invoked.
+ // * If it returns a value, that becomes the result.
+ // * If upcast, a return value of the supertype is accepted.
+ // * If it throws a new error, that is the error result.
+ // * If it throws the original error *synchronously*,
+ // the original stack trace is retained.
+ // * If it throws the original error *asynchronously*,
+ // the original stack trace is not expected to be retained.
+ for (var native in [true, false]) {
+ var nativity = native ? "native" : "non-native";
+ var future = native ? errorFuture : nonNativeErrorFuture;
+ for (var upcast in [true, false]) {
+ var upcastText = upcast ? " as Future<num?>" : "";
+ // "fails" means not to match type parameter.
+ // Rest means match type parameter, a bool is what `test` returns,
+ // `null` means no test function.
+ for (var testKind in ["fails", true, false, null]) {
+ var matches = testKind == true || testKind == null;
+ // If `matches` is false, the error handler should not be invoked.
+ // Don't bother with creating sync/async versions of it then,
+ // just one which fails if called.
+ for (var async in [if (matches) true, false]) {
+ var asyncText = async ? "async" : "sync";
+ // Action of the handler body. If test doesn't match, it will fail.
+ for (var action
+ in matches ? ["return", "throw", "rethrow"] : ["fail"]) {
+ // Called to provide the necessary types as type parameters.
+ void doTest<E extends Object, T>() {
+ String testName =
+ "$nativity Future<int?>.error(StateError(..))$upcastText ";
+
+ bool Function(E)? testFunction;
+ switch (testKind) {
+ case "fails":
+ testFunction = (E value) {
+ Expect.fail("$testName: Matched error");
+ throw "unreachable"; // Expect.fail throws.
+ };
+ break;
+ case true:
+ case false:
+ var testResult = testKind == true;
+ testFunction = (E value) => testResult;
+ testName +=
+ "matches type $E ${testResult ? "and test" : "but not test"}";
+ break;
+ default:
+ testName += "matches type with no test";
+ }
+ Object? expectation = expectedError;
+ FutureOr<T> Function(E, StackTrace) onError;
+ switch (action) {
+ case "return":
+ expectation = upcast ? 3.14 : 42;
+ testName += " and returns $expectation";
+ onError = (E e, StackTrace s) {
+ return expectation as T;
+ };
+ break;
+ case "throw":
+ expectation = expectedOtherError;
+ onError = (E e, StackTrace s) {
+ throw otherError;
+ };
+ testName += " and throws new error";
+ break;
+ case "rethrow":
+ onError = (E e, StackTrace s) {
+ throw e;
+ };
+ testName += " and rethrows";
+ // Asynchronous callbacks rethrowing the error
+ // doesn't count as a rethrow, only synchronous throws.
+ if (async) expectation = AsyncError(error, StackTrace.empty);
+ break;
+ case "fail":
+ onError = (E e, StackTrace s) {
+ Expect.fail("$testName: Matched error");
+ throw "unreachable"; // Expect.fail throws.
+ };
+ testName += " and rethrows original error";
+ break;
+ default:
+ throw "unreachable";
+ }
+ if (async) {
+ testName += " asynchronously";
+ var originalOnError = onError;
+ onError = (E e, StackTrace s) async => originalOnError(e, s);
+ }
+ test<E, T>(testName, future as Future<T>, onError,
+ test: testFunction, result: expectation);
+ }
+
+ // Find the types to use.
+ if (testKind == "fails") {
+ if (upcast) {
+ doTest<Exception, num?>();
+ } else {
+ doTest<Exception, int?>();
+ }
+ } else {
+ if (upcast) {
+ doTest<StateError, num?>();
+ } else {
+ doTest<StateError, int?>();
+ }
+ }
+ }
+ }
+ }
+ }
}
- {
- // With failing test.
- asyncStart();
- var future = Future.error(error, stack);
- var onErrorFuture = future.onError((e, s) {
- Expect.fail('unreachable');
- }, test: (o) => false);
- onErrorFuture.catchError((e, s) {
- Expect.identical(error, e);
- Expect.identical(stack, s);
- asyncEnd();
- });
- }
-
- {
- // With matching type.
- asyncStart();
- var future = Future.error(stateError, stack);
- future.onError<StateError>((e, s) {
- Expect.identical(stateError, e);
- Expect.identical(stack, s);
- asyncEnd();
- });
- }
-
- {
- // With non-matching type.
- asyncStart();
- var future = Future.error(stateError, stack);
- var onErrorFuture = future.onError<ArgumentError>((e, s) {
- Expect.fail('unreachable');
- });
- onErrorFuture.catchError((e, s) {
- Expect.identical(stateError, e);
- Expect.identical(stack, s);
- asyncEnd();
- });
- }
-
- {
- // With non-matching type and matching test.
- asyncStart();
- var future = Future.error(stateError, stack);
- var onErrorFuture = future.onError<ArgumentError>((e, s) {
- Expect.fail('unreachable');
- }, test: (ArgumentError e) => true);
- onErrorFuture.catchError((e, s) {
- Expect.identical(stateError, e);
- Expect.identical(stack, s);
- asyncEnd();
- });
- }
-
- {
- // With matching type and matching test.
- asyncStart();
- var future = Future.error(stateError, stack);
- future.onError<StateError>((e, s) {
- Expect.identical(stateError, e);
- Expect.identical(stack, s);
- asyncEnd();
- }, test: (StateError e) => true);
- }
-
- {
- // With matching type and non-matching test.
- asyncStart();
- var future = Future.error(stateError, stack);
- var onErrorFuture = future.onError<StateError>((e, s) {
- Expect.fail('unreachable');
- }, test: (StateError e) => false);
- onErrorFuture.catchError((e, s) {
- Expect.identical(stateError, e);
- Expect.identical(stack, s);
- asyncEnd();
- });
- }
asyncEnd();
}
+
+class NonNativeFuture<T> implements Future<T> {
+ Future<T> _original;
+ NonNativeFuture.value(T value) : _original = Future<T>.value(value);
+ NonNativeFuture.error(Object error, [StackTrace? stack])
+ : _original = Future<T>.error(error, stack)..ignore();
+ NonNativeFuture._(this._original);
+ Future<R> then<R>(FutureOr<R> Function(T) handleValue, {Function? onError}) =>
+ NonNativeFuture<R>._(_original.then(handleValue, onError: onError));
+ Future<T> catchError(Function handleError, {bool Function(Object)? test}) =>
+ NonNativeFuture<T>._(_original.catchError(handleError, test: test));
+ Future<T> whenComplete(FutureOr<void> Function() onDone) =>
+ NonNativeFuture<T>._(_original.whenComplete(onDone));
+ Future<T> timeout(Duration timeLimit, {FutureOr<T> Function()? onTimeout}) =>
+ NonNativeFuture<T>._(_original.timeout(timeLimit, onTimeout: onTimeout));
+ Stream<T> asStream() => _original.asStream();
+}