Skip to content

Disallow returning futures from async functions. #870

@lrhn

Description

@lrhn

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, dart-lang/sdk#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.

Metadata

Metadata

Assignees

Labels

featureProposed language feature that solves one or more problems

Type

No type

Projects

Status

Being discussed

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions