value-free return should be allowed in function which returns FutureOr<void>
According to the current spec, only void, dynamic and Null allow for return;. I'd like to add FutureOr<void> (and FutureOr<void>??) to this list.
Don't be alarmed, my motivation is not for users making FutureOr APIs, etc. My motivation is Future.catchError, see https://github.com/dart-lang/sdk/issues/35825.
The onError parameter of Future<T>.catchError is typed as Function, because the acceptable signatures of that function cannot be expressed right now. onError can either be FutureOr<T> Function(dynamic) or FutureOr<T> Function(dynamic, StackTrace). In either case the return type is FutureOr<T>.
I think it makes sense to be able to write a Future<void>.catchError handler with return; statements (Flutter engine does it in a few places).
More generally, and out of correctness, it seems inconsistent that FutureOr<void> f() {} is legal, a function body that falls off the edge without returning a value, but FutureOr<void> f() { return; } is illegal.
Related issues: At first I thought this was an analyzer bug (https://github.com/dart-lang/sdk/issues/44480); other catchError discussion at https://github.com/dart-lang/sdk/issues/34144
More generally, and out of correctness, it seems inconsistent that
FutureOr<void> f() {}is legal, a function body that falls off the edge without returning a value, butFutureOr<void> f() { return; }is illegal.
+1.
The
onErrorparameter ofFuture<T>.catchErroris typed asFunction, because the acceptable signatures of that function cannot be expressed right now.onErrorcan either beFutureOr<T> Function(dynamic)orFutureOr<T> Function(dynamic, StackTrace). In either case the return type isFutureOr<T>.
This sounds like a strong argument in favor of handling the situation more gracefully, but the type inference that occurs in such a situation actually gives the function literal a return type of Null rather than FutureOr<void>:
import 'dart:async';
void try1370(FutureOr<void> Function() f) {
print(f.runtimeType); // '() => Null'.
}
void main() async {
try1370(() { return; }); // No diagnostics.
}
There is no error because the inferred return type is Null rather than FutureOr<void>, and return; is OK in a function whose return type is Null.
Function literal type inference doesn't copy the return type from the context type, it (roughly) computes the return type bottom-up from the actually returned expressions (adding in Null if the end of the body is reachable, and adding in Null if it contains a return statement of the form return;).
This means that we only have the case where the return type is specified explicitly, as in the original example:
import 'dart:async';
FutureOr<void> f1() {
return; // error: return_without_value
}
In this case I'd recommend that f1 is modified to use a different return type in the first place (see also avoid_futureor_void). If the intention is that the caller should ignore the returned object in all cases then the return type could be void.
If the intention is that the function can return a future (which should then be awaited, and not ignored), or it can return something which should be ignored, then the return type can be Future<void>?. This is a well-defined choice because no object can have both the type Null and any type of the form Future<...> at the same time.
On the other hand, if an expression has the type FutureOr<void> then we can never know how to treat it: Any object whatsoever has type void, so we should ignore it. However, some objects also have type Future<S> for some S, and for those objects we can't know whether it was intended to be ignored or awaited.
So we could relax the specification to say that return; is allowed in a function whose return type is dynamic or Null, or it is a type T such that its "FutureOr base" is void (that is, T is void, or it is FutureOr<void>, or it is FutureOr<FutureOr<void>>, etc.). However, I don't particularly want to promote the use of FutureOr<void> as a type at all, and I don't think we have any known situations where the need is really compelling.