More elaborate error when a future error handler returns something of the wrong type.

Bug: https://github.com/dart-lang/sdk/issues/44386, https://github.com/dart-lang/sdk/issues/41313
Change-Id: I87b10c3cd03475f4cd80b7a7c5cba6a558167748
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/175062
Reviewed-by: Nate Bosch <nbosch@google.com>
Commit-Queue: Lasse R.H. Nielsen <lrn@google.com>
diff --git a/sdk/lib/async/future_impl.dart b/sdk/lib/async/future_impl.dart
index c89d345..d005f24 100644
--- a/sdk/lib/async/future_impl.dart
+++ b/sdk/lib/async/future_impl.dart
@@ -170,13 +170,32 @@
     var errorCallback = this.errorCallback; // To enable promotion.
     // If the errorCallback returns something which is not a FutureOr<T>,
     // this return statement throws, and the caller handles the error.
+    dynamic result;
     if (errorCallback is dynamic Function(Object, StackTrace)) {
-      return _zone.runBinary<dynamic, Object, StackTrace>(
+      result = _zone.runBinary<dynamic, Object, StackTrace>(
           errorCallback, asyncError.error, asyncError.stackTrace);
     } else {
-      return _zone.runUnary<dynamic, Object>(
+      result = _zone.runUnary<dynamic, Object>(
           errorCallback as dynamic, asyncError.error);
     }
+    // Give better error messages if the result is not a valid
+    // FutureOr<T>.
+    try {
+      return result;
+    } on TypeError {
+      if (handlesValue) {
+        // This is a `.then` callback with an `onError`.
+        throw ArgumentError(
+            "The error handler of Future.then"
+                " must return a value of the returned future's type",
+            "onError");
+      }
+      // This is a `catchError` callback.
+      throw ArgumentError(
+          "The error handler of "
+              "Future.catchError must return a value of the future's type",
+          "onError");
+    }
   }
 
   dynamic handleWhenComplete() {
@@ -315,10 +334,20 @@
 
   Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
     Zone currentZone = Zone.current;
-    if (!identical(currentZone, _rootZone)) {
+    if (identical(currentZone, _rootZone)) {
+      if (onError != null &&
+          onError is! Function(Object, StackTrace) &&
+          onError is! Function(Object)) {
+        throw ArgumentError.value(
+            onError,
+            "onError",
+            "Error handler must accept one Object or one Object and a StackTrace"
+                " as arguments, and return a value of the returned future's type");
+      }
+    } else {
       f = currentZone.registerUnaryCallback<FutureOr<R>, T>(f);
       if (onError != null) {
-        // In checked mode, this checks that onError is assignable to one of:
+        // This call also checks that onError is assignable to one of:
         //   dynamic Function(Object)
         //   dynamic Function(Object, StackTrace)
         onError = _registerErrorHandler(onError, currentZone);
@@ -873,9 +902,9 @@
   if (errorHandler is dynamic Function(Object)) {
     return zone.registerUnaryCallback<dynamic, Object>(errorHandler);
   }
-  throw new ArgumentError.value(
+  throw ArgumentError.value(
       errorHandler,
       "onError",
       "Error handler must accept one Object or one Object and a StackTrace"
-          " as arguments, and return a valid result");
+          " as arguments, and return a value of the returned future's type");
 }
diff --git a/sdk/lib/async/stream.dart b/sdk/lib/async/stream.dart
index 4851883..d03e095 100644
--- a/sdk/lib/async/stream.dart
+++ b/sdk/lib/async/stream.dart
@@ -661,6 +661,14 @@
   /// If a broadcast stream is listened to more than once, each subscription
   /// will individually perform the `test` and handle the error.
   Stream<T> handleError(Function onError, {bool test(error)?}) {
+    if (onError is! void Function(Object, StackTrace) &&
+        onError is! void Function(Object)) {
+      throw ArgumentError.value(
+          onError,
+          "onError",
+          "Error handler must accept one Object or one Object and a StackTrace"
+              " as arguments.");
+    }
     return new _HandleErrorStream<T>(this, onError, test);
   }
 
diff --git a/sdk/lib/async/stream_pipe.dart b/sdk/lib/async/stream_pipe.dart
index 60aff7e..4533043 100644
--- a/sdk/lib/async/stream_pipe.dart
+++ b/sdk/lib/async/stream_pipe.dart
@@ -243,14 +243,11 @@
 /// A stream pipe that converts or disposes error events
 /// before passing them on.
 class _HandleErrorStream<T> extends _ForwardingStream<T, T> {
-  final Function _transform;
+  final Function _onError;
   final bool Function(Object)? _test;
 
-  _HandleErrorStream(
-      Stream<T> source, Function onError, bool test(Object error)?)
-      : this._transform = onError,
-        this._test = test,
-        super(source);
+  _HandleErrorStream(Stream<T> source, this._onError, this._test)
+      : super(source);
 
   void _handleData(T data, _EventSink<T> sink) {
     sink._add(data);
@@ -269,7 +266,7 @@
     }
     if (matches) {
       try {
-        _invokeErrorHandler(_transform, error, stackTrace);
+        _invokeErrorHandler(_onError, error, stackTrace);
       } catch (e, s) {
         if (identical(e, error)) {
           sink._addError(error, stackTrace);
diff --git a/sdk/lib/core/errors.dart b/sdk/lib/core/errors.dart
index 683267d..3770a2b 100644
--- a/sdk/lib/core/errors.dart
+++ b/sdk/lib/core/errors.dart
@@ -146,11 +146,16 @@
   /// Existing code may be using `message` to hold the invalid value.
   /// If the `message` is not a [String], it is assumed to be a value instead
   /// of a message.
+  ///
+  /// If [name] is provided, it should be the name of the parameter
+  /// which was invalid.
+  ///
+  /// Consider using [ArgumentError.value] instead to retain and document the
+  /// invalid value as well.
   @pragma("vm:entry-point")
-  ArgumentError([this.message])
+  ArgumentError([this.message, @Since("2.14") this.name])
       : invalidValue = null,
-        _hasValue = false,
-        name = null;
+        _hasValue = false;
 
   /// Creates error containing the invalid [value].
   ///