-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Disallow returning futures from async
functions.
#870
Comments
If we allow implicit cast from |
No static warnings for Currently the expression should first be evaluated to an object, then that object should be checked for being a The issue here is not new, it's just that it might hide something which becomes a run-time error. |
I think implementing this as analyzer rule is better, at least it doesn't require migration. |
BTW, this is the opposite of what the |
This is not the same case.
Future<int> future;
Future<int> f1() => future; over Future<int> future;
Future<int> f1() async => await future; Both, however, would be valid in this issue proposal, as the first case is not What would not be valid is Future<int> future;
Future<int> f1() async => future; |
Yes. The lint will need to be changed to only catch the case mentioned by @mateusfccp but allow it otherwise. |
There is a subtle inference detail that I think this could help with. Currently the inference type that gets used for an expression which is returned is T someMethod<T>(T arg) {
print(T);
return arg;
}
Future<S> returnIt<S>(S arg) async {
return someMethod(arg); // Infers someMethod<FutureOr<T>>
}
Future<S> assignIt<S>(S arg) async {
final result = someMethod(arg); // Infers someMethod<T>
return result;
}
void main() {
returnIt(1);
assignIt(1);
} If we require the |
We could add a hint in the analyzer ahead of this change so that existing code can start to migrate ahead of time. |
I really want to make this happen, preferably for Dart 3.0, but it's a potentially perilous migration. I am working on a test of the viability of a particular migration strategy:
I want to check how viable it is to perform that migration before removing the current implicit await behavior, Being successful depends on not having cases where we return a I just need to learn how to make a front-end transformation, ... that'll be any day now! |
My current best guess is that this will not be considered safe enough in time to be included in Dart 3.0. What I do want to do is:
The risk is mainly around code returning Future<dynamic> getJson(File file) async => jsonDecode(await file.readAsString); which wants to return There are potential, hypothetical, cases where a |
@nshahan @sigmundch @aam - I wonder what are the reasons that made flutter use this rule. Should we be concerned about potential performance/code size issues following this change? If yes, are optimizations possible? |
cc @alexmarkov @mkustermann - I believe you have both looked at async performance in the past on the VM side. Do you have any concerns here around this language proposal to force using |
An additional Ideally we should pair this change with only allowing to These two changes (disallow returning futures and disallow await non-futures) would make overall language semantics clearer, async Dart code more strictly typed and would allow users to avoid error-prone cases like working with In addition, in order to get back any regressed performance caused by this change we can optimize |
Agreed! - especially that it would
However, I'd like to comment on the performance overhead:
The typical case should actually be that there is no additional Moreover, in many of these cases the static type of The case where the returned expression has static type The only case where there is an extra cost is the following: We encounter This is an extra cost (because we could just have used the value of For example: import 'dart:async';
class A {}
class B1 extends A {}
class B2 extends A implements Future<B1> {
Future<B1> fut = Future.value(B1());
asStream() => fut.asStream();
catchError(error, {test}) => fut.catchError(error, test: test);
then<R>(onValue, {onError}) => fut.then(onValue, onError: onError);
timeout(timeLimit, {onTimeout}) =>
fut.timeout(timeLimit, onTimeout: onTimeout);
whenComplete(action) => fut.whenComplete(action);
}
Future<A> g(A a) async {
var a2 = await a; // `a2` has static type `A`, and the future is awaited.
return a2;
}
void main() async {
var a = await g(B2());
print(a is B1); // Prints 'true'.
} If the body of I suspect that it is an exceedingly rare situation that an object has a run-time type which is a subtype of All in all, I think this amounts to a rather strong argument that there is no real added performance cost. Moreover, the cases where there is an actual extra cost are likely to be rare, error-prone, and confusing, and it's probably much better to require that |
Maybe that's how it is specified, but not how it is implemented and how it works today. Currently VM doesn't perform an implicit The following example demonstrates the point: foo() async {
await null;
throw 'Err';
}
void main() async {
try {
return foo();
} catch(e) {
print('Caught: $e');
}
} Changing However, as I mentioned above, we can alleviate this performance overhead by adding an optimization which would combine |
Oops, that's a bug: dart-lang/sdk#44395. I thought that had been fixed, but there are many failures: https://dart-ci.firebaseapp.com/current_results/#/filter=language/async/return_throw_test. |
As is, the `finally` block gets run before the future returned by the callback has completed. I believe the underlying issue here is this one in the Dart SDK: dart-lang/sdk#44395 The fix is to await the future and return the result of that, rather than returning the future directly. The Dart language folks (or at least some of them) hope to eventually deal with this by making the old code here a type error, requiring the value passed to `return` to have type T rather than Future<T>: dart-lang/language#870
Well, Just waking this up from dart-lang/sdk#54311 and listening for this to land |
If, in the future, Dart doesn't allow returning a Future without await inside an async block, it could potentially break a lot of code. IMHO I consider it a bug only if it's inside a "try" block, as without it (try block), the behavior remains the same with or without await. It might be challenging to implement this language change, and it could take a while to release it. A lint could help resolve the issue for now, in a simple and compatible way. Also, consider allowing the return of Future (or anything) without await for a method returning FutureOr to avoid disrupting optimizations, since a method that returns FutureOr usually is attempting to optimize things. See https://pub.dev/packages/async_extension |
I dont really get the part with and without try bloc @gmpassos Can u elaborate it |
Currently a try block only catches an exception of a Future if you use await before return it. See: |
Thanks for explaining, understood. As of now implicilty a await is being performed on return value. But if we remove the await the future would have no context as like before. |
Well, I think that this new behavior could generate many side effects. At least this need to be well tested and the collateral effects well documented, including performance issues, due the overhead. |
@gmpassos wrote:
Right, the bug dart-lang/sdk#44395 is the behavior during execution of a
I think 'being optimized' in this context means 'executes with no asynchronous suspensions', that is, "it just runs normally all the way". In general, it is not safe to assume that code which is possibly-asynchronous would actually run without any suspensions (for instance, whether or not there's an extra suspension when an The safe way to write code which is supposed to run without asynchronous suspensions is to make it fully non-asynchronous. You could then have a non-async function which has return type However, it is inconvenient that this "strict" approach doesn't fit packages like async_extension very well. (From a brief look at it, I assume that this package is directly aimed at writing code which is "potentially asynchronous", and still obtain a non-suspended execution whenever possible. My advice is basically "don't try to do that, it's too difficult to ensure that it will actually never suspend, in the situations where that's expected.") |
Note that in the case of a method that returns FutureOr, I'm more concerned about methods without "async", to ensure that they are not affected by the new behavior (sorry for not being clearer). For me an async method, returning FutureOr, should behave exactly like a method that returns Future. Also there's no meaning to have a method that is async and returns a FutureOr, unless you are respecting some "interface" signature. |
Such a function will indeed return a
Exactly! You might be forced into using |
In an
async
function with return typeFuture<T>
, Dart currently allow you to return either aT
orFuture<T>
.That made (some) sense in Dart 1, where the type system wasn't particularly helpful and we didn't have type inference. Also, it was pretty much an accident of implementation because the return was implemented as completing a
Completer
, andCompleter.complete
accepted both a value and a future. If thecomplete
method had only accepted a value, then I'm fairly sure the language wouldn't have allowed returning a future either.In Dart 2, with its inference pushing context types into expressions, the
return
statement accepting aFutureOr<T>
is more of a liability than an advantage (see, fx, dart-lang/sdk#40856).I suggest that we change Dart to not accept a
Future<T>
in returns in anasync
function.Then the context type of the return expression becomes
T
(the "future return type" of the function).The typing around returns gets significantly simpler. There is no flatten on the expression, and currently an
async
return needs to check whether the returned value is aFuture<T>
, and if so, await it.If
T
isObject
, then that's always a possibility, so every return needs to dynamically check whether the value is a future, even if the author knows it's not.This is one of the most complicated cases of implicit future handling in the language specification, and we'd just remove all the complication in one fell swoop.
And it would improve the type inference for users.
It would be a breaking change.
Any code currently returning a
Future<T>
or aFutureOr<T>
will have to insert an explicitawait
.This is statically detectable.
The one case which cannot be detected statically is returning a top type in a function with declared return type
Future<top type>
. Those needs to be manually inspected to see whether they intend to wait for any futures that may occur.Alternatively, we can always insert the
await
in the migration, since awaiting non-futures changes nothing. It would only be a problem if the return type isFuture<Future<X>>
and thedynamic
-typed value is aFuture<X>
. AFuture<Future<...>>
type is exceedingly rare (and shouldn't happen, ever, in well-designed code), so always awaitingdynamic
is probably a viable approach. It may change timing, which has its own issues for badly designed code that relies on specific interleaving of asynchronous events.That is the entirety of the migration, and it can be done ahead of time without changing program behavior*. We could add a lint discouraging returning a future, and code could insert
awaits
to get rid of the lint warning. Then they'd be prepared for the language change too.The change would remove a complication which affects both our implementation and our users negatively, It would make an implicit await in returns into an explicit await, which will also make the code more readable, and it will get rid of the implementation/specification discrepancy around
async
returns.*: Well, current implementations actually do not await the returned value, as the specification says they should, which means that an error future can get past a
try { return errorFuture; } catch (e) {}
. That causes much surprise and very little joy when it happens. I've also caught mistakes related to this in code reviews.The text was updated successfully, but these errors were encountered: