language
language copied to clipboard
Disallow returning futures from `async` functions.
In an async
function with return type Future<T>
, Dart currently allow you to return either a T
or Future<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
, and Completer.complete
accepted both a value and a future. If the complete
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 a FutureOr<T>
is more of a liability than an advantage (see, fx, https://github.com/dart-lang/sdk/issues/40856).
I suggest that we change Dart to not accept a Future<T>
in returns in an async
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 a Future<T>
, and if so, await it.
If T
is Object
, 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 a FutureOr<T>
will have to insert an explicit await
.
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 is Future<Future<X>>
and the dynamic
-typed value is a Future<X>
. A Future<Future<...>>
type is exceedingly rare (and shouldn't happen, ever, in well-designed code), so always awaiting dynamic
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.
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.
If we allow implicit cast from dynamic
we wouldn't warn for any cases where the expression has static type dynamic
, right? Would we special case that situation?
No static warnings for dynamic
, as usual.
The expression would be implicitly cast to T
before being returned, just as a return statement in a non-async function.
Currently the expression should first be evaluated to an object, then that object should be checked for being a Future<T>
, and if it is, it is awaited and the result becomes the new value, otherwise we use the original value. Then the resulting value is implicitly cast to T
.
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 unnecessary_await_in_return
lint recommends, so if this is done, the lint will need to be removed or disabled, and maybe analysis_options.yaml
files will need to be updated during migration.
BTW, this is the opposite of what the
unnecessary_await_in_return
lint recommends, so if this is done, the lint will need to be removed or disabled, and maybeanalysis_options.yaml
files will need to be updated during migration.
This is not the same case.
unnecessary_await_in_return
prefers
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 async
and returns a Future<T>
, and the second case is async
and return a T
(because of await
).
What would not be valid is
Future<int> future;
Future<int> f1() async => future;
BTW, this is the opposite of what the
unnecessary_await_in_return
lint recommends, so if this is done, the lint will need to be removed or disabled
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 FutureOr
, which may not be what the user expects. If there is a call to a generic method as the returned expression it can have user visible impacts on the inference.
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 await
to get to a T
, that inference will work like user expectations.
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:
- Add
await
to anyreturn e
wheree
has a static type implementingFuture
or beingFutureOr
. - Do nothing for a
return e
wheree
has any other type, other thandynamic
- For a dynamic return, change the return to
var $tmp = e; return $tmp is Future<E> ? await $tmp : $tmp as E;
. (Basically do explicitly what we now do implicitly in thereturn
.)
I want to check how viable it is to perform that migration before removing the current implicit await behavior, as well as how well it preserves the current behavior after removing the current awaiting behavior.
Being successful depends on not having cases where we return a Future<Future<X>>
and where this behavior would then await twice. Hopefully nested futures will be rare. You should never, ever have nested futures. If you do, your design should be improved instead. (Ob-nerd: ◇◇φ ⇔ ◇φ)
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:
- Introduce a lint telling you to insert the
await
if you are returning something which would accept theawait
(aFuture
orFutureOr
type which will be awaited anyway). Preferably with a quickfix/autofix. - Get that into the
recommended.yaml
lints. - Pass time while people get used to it.
- Then make a later language versioned change change to change the behavior of return in
await
.
The risk is mainly around code returning dynamic
. We can't see if that's intended to be FutureOr
or Future
or not.
We can consider a "strict_async_returns" lint which forces you to cast to a non-dynamic
type, but that's annoying for
Future<dynamic> getJson(File file) async => jsonDecode(await file.readAsString);
which wants to return dynamic
(and knows it cannot be a Future
). I guess we'll just have to do some trial runs and see how much breaks, and if we can detect and fix it.
There are potential, hypothetical, cases where a return typedAsObject
can contain a Future<Object>
, but that's such a bad design that it's more likely to be an error than a deliberate choice.
@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 await
when returning a future from an async function?
An additional await
at return
will add certain performance overhead, which may regress performance in case async method sometimes needs to delegate to another async method. However, it would also help to avoid runtime type check which is used when compiler cannot statically prove that return value is not future. That has a potential for improving performance in more common cases.
Ideally we should pair this change with only allowing to await
futures. That would eliminate potentially costly runtime type check from await
too and adding await
at return
would have less performance overhead (see https://github.com/dart-lang/sdk/issues/49396).
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 Future<Future<X>>
. So it should be a win for user experience too.
In addition, in order to get back any regressed performance caused by this change we can optimize return await <expr>
pattern in compiler if it is not inside try
block to skip suspending current activation and forwarding result without resuming the current activation.
Agreed! - especially that it would
make overall language semantics clearer
However, I'd like to comment on the performance overhead:
An additional
await
atreturn
will add certain performance overhead
The typical case should actually be that there is no additional await
: The statement return e;
in an async
function body will need to be changed to return await e;
in the situation where the current semantics would potentially evaluate an await
expression anyway.
Moreover, in many of these cases the static type of e
is Future<T>
for some T
which is a subtype of the future value type of the enclosing function, and in this case it's guaranteed that return e;
in the current semantics would evaluate an await
expression. In this case return e;
with the old semantics and return await e;
with the new semantics will have exactly the same behavior at run time. No extra cost.
The case where the returned expression has static type dynamic
is special, and the code transformation which will ensure that the semantics is unchanged will contain an await
expression, and it may or may not be executed. No extra cost here, though, because the rewritten code with the new semantics can have exactly the same behavior as return e;
with the old semantics.
The only case where there is an extra cost is the following: We encounter return e;
in an async
function body with future value type F
, e
has a static type which is a subtype of F
(so return e;
is type correct, and it looks like we can just complete the already-returned future with the value of e
). However, it is still possible that the value of e
has a run-time type which is a subtype of Future<F>
, and in that case we must await the future (according to the current semantics). So if we follow the general rule and change return e;
to return await e;
and run it with the new semantics, we might evaluate await o
where o
is an object which is not a future (or not a Future<F>
, at least, where F
is the future value type of the enclosing function).
This is an extra cost (because we could just have used the value of e
directly). But, crucially, it's a different semantics if we don't perform the await, because awaiting that future will potentially have side effects and it will almost certainly yield a different object. That object could have a different run-time type as well (although it must still be some subtype of F
).
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 g
had been return a;
then the old semantics would have performed an await a
at run time, even though the value of a
is already a type correct completion of the already-returned future. So the actual body of g
above spells out the meaning of return a;
with the old semantics. However, with the new semantics we could also write return a;
, in which case we would complete the already-returned future with the given A
(whose run-time type is B2
), and we would not have awaited the future.
I suspect that it is an exceedingly rare situation that an object has a run-time type which is a subtype of T
as well as a subtype of Future<T>
, for any T
which isn't Object
or a top type. I also suspect that these situations are rather error prone, and it's actually quite surprising that we can receive that instance of A
(which is a class that has no relation to Future
), then discover that it happens to be a Future<A>
as well, and then await it.
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 await
operation to be requested explicitly by developers, or explicitly not requested, and then we can all see what's going on! :smile:
The typical case should actually be that there is no additional
await
: The statementreturn e;
in anasync
function body will need to be changed toreturn await e;
in the situation where the current semantics would potentially evaluate anawait
expression anyway.Moreover, in many of these cases the static type of
e
isFuture<T>
for someT
which is a subtype of the future value type of the enclosing function, and in this case it's guaranteed thatreturn e;
in the current semantics would evaluate anawait
expression. In this casereturn e;
with the old semantics andreturn await e;
with the new semantics will have exactly the same behavior at run time. No extra cost.
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 await
at return. Instead, if return value is a future, result future for async
function is chained to complete when return value completes. This is more efficient than await
, so an explicit await
at return
would incur additional performance overhead.
The following example demonstrates the point:
foo() async {
await null;
throw 'Err';
}
void main() async {
try {
return foo();
} catch(e) {
print('Caught: $e');
}
}
Changing return foo()
to return await foo()
in this example shows the difference.
However, as I mentioned above, we can alleviate this performance overhead by adding an optimization which would combine return await
to the future chaining if return await
is not inside try
block. So potential performance overhead of additional await
shouldn't stop us from making this language change.
Oops, that's a bug: https://github.com/dart-lang/sdk/issues/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.
Well, Just waking this up from https://github.com/dart-lang/sdk/issues/54311 and listening for this to land cc: @lrhn
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:
https://github.com/dart-lang/linter/issues/4833
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:
IMHO I consider it a bug only if it's inside a "try" block
Right, the bug dart-lang/sdk#44395 is the behavior during execution of a return e;
statement that occurs inside a try
block in an async
body, there's no known malfunction in this area for other placements of return e;
.
... a method that returns FutureOr usually is attempting to optimize things
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 async
function body returns a future). The behavior of the scheduler is deliberately specified with some wiggle room for implementations (for instance, the language specification doesn't even mention a scheduler, it just says that the execution must be suspended in certain situations).
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 FutureOr<T>
for a suitable T
, and it would run asynchronous code and return a T
in its fast paths. It would then also have some execution paths where the result is a Future<T>
(and that might well be expressed as a different function with an async
body, in order to allow for await
expressions).
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.
For me an async method, returning FutureOr, should behave exactly like a method that returns Future.
Such a function will indeed return a Future<...>
no matter which return type we specify (as long as that return type isn't a compile-time error). The actual type argument of that future is based on the declared return type, so if the function declares that it returns a FutureOr<T>
then it will actually return a Future<T>
.
Also there's no meaning to have a method that is async and returns a FutureOr, unless you are respecting some "interface" signature.
Exactly! You might be forced into using FutureOr<T>
as the return type of some instance method with an async
body, because it is a participant in an override relationship where some overriding declarations will actually return a T
(and those overriding declarations might then have return type T
, or they might also use FutureOr<T>
, if they are overridden by a declaration that actually will return a Future<T>
).