Dart offers a FutureOr class, that allows writing:
FutureOr<int> future; future = 42; // valid future = Future.value(42); // also valid
I would assume that FutureOr would be useful to remove the unnecessary delay caused by the event loop if the value can be read synchronously.
But that doesn't seem to be the case, as showcased by:
import 'dart:async'; void main() async { print('START'); futureOrExample(); print('END'); } void futureOrExample() async { FutureOr<int> futureOr = 42; print('before await'); await futureOr; print('end await'); }
which prints:
START before await END end await
when I would expect:
START before await end await END
In that case, why does FutureOr (or more generally await 42
) work this way?
Similarly, what's the purpose of FutureOr in that situation since it produces the same result as Future?
I know that I could use SynchronousFuture to achieve the desired result, but I'm just trying to understand what's the use of FutureOr.
The use of FutureOr
, as introduced with Dart 2, is to allow you to provide either a value or a future at a point where the existing Dart 1 API allowed the same thing for convenience, only in a way that can be statically typed.
The canonical example is Future.then
. The signature on Future<T>
is Future<R> then<R>(FutureOr<R> action(T value), {Function onError})
.
The idea is that you can have an action on the future's value which is either synchronous or asynchronous. Originally there was a then
function which took a synchronous callback and a chain
function which took an asynchronous callback, but that was highly annoying to work with, and in good Dart 1 style, the API was reduced to one then
method which took a function returning dynamic
, and then it checked whether it was a future or not.
In Dart 1 it was easy to allow you to return either a value or a future. Dart 2 was not as lenient, so the FutureOr
type was introduced to allow the existing API to keep working. If we had written the API from scratch, we'd probably have done something else, but migrating the existing asynchronous code base to something completely different was not an option, so the FutureOr
type was introduced as a type-level hack.
The await
operation was also originally defined to work on any object, long before FutureOr
existed. For consistency and smaller code, an await e
where e
evaluated to a non-future would wrap that value in a future and await that. It means that there is only one quick and reusable check on a value (is it a future, if not wrap it), and then the remaining code is the same. There is only one code-path. If the await
worked synchronously on non-Future
values, there would have to be a synchronous code path running through the await
, as well as an asynchronous path waiting for a future. That would potentially double the code size, for example when compiling to JavaScript (or worse, if there were more await
s in the same control flow, you could get exponential blow-up for a naive implementation). Even if you avoided that by just calling the continuation function synchronously, it would likely be confusing to some readers that an await
would not introduce an asynchronous gap. A mistake around that can cause race conditions or things happening in the wrong order.
So, the original design, predating FutureOr
, was to make all await
operations actually wait.
The introduction of FutureOr
did not change this reasoning, and even if it did, it would now be a breaking change to not wait in places where people expect their code to actually give time for other microtasks to run.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With