Legitimate use case to detect if a Future is completed.

I’m the creator of https://pub.dev/packages/async_redux and we let devs create sync or async reducers depending on the return type of their reducer:

FutureOr<AppState> reduce();

In other words, they may return AppState or Future<AppState>, and I process it like this:

var result = action.reduce();

if (result is Future<St>) {
  return result.then((state) => registerState(state));
} else if (result is St) {
  registerState(result);
}

This works fine, unless they try to return a completed Future. For example, this is WRONG:

Future<AppState> reduce() async { return state;}

The solution is returning only Future which are NOT completed, for example:

Future<AppState> reduce() async { await someFuture(); return state;}

Which is totally fine. The reason completed futures don’t work is because the new returned state must be registered in the same microtask. If it waits even a microtask, the state may get changed by some other reducer, and that previous state will be written over with stale data.

In other words, I need the then to be executed in the same microtask of the returned value, which ONLY happens if the Future returned by the reducer is NOT completed.

I read here #14323 the following:

Another argument against immediate callback on listen is that it gives you two programming models – immediate callback and eventual callback. You can write code that assumes immediate callback, and that breaks if the callback comes later. And you can write code that assumes a later callback, and breaks if the callback comes immediately (which did happen in the beginning). (…) Sticking with one model: callbacks are always later, working with futures becomes much simpler.

This is all fine, but the problem is: Callbacks are always later, yes, but you can never be sure that the returned value is within the same microtask or not. You made it easy to reason about when the callback is called (later), but you made it impossible to reason about if the value it is getting is recent or stale. If you assume it’s from the same microtask you may be wrong, and if you assume it’s from the previous microtask you also may be wrong. There’s absolutely no way of knowing, so you don’t know if that returned value is stale or not.

At the moment I have to trust the developers to do the right thing, and there is absolutely no way for me to know if they did something wrong.

What I need to do is this:

if (result is Future<St>) {
  if (result.isComplete) throw AssertionError("Don't return completed Futures.");
  return result.then((state) => registerState(state));
}

Is there any reason why _isComplete is private in _Future in future_impl.dart?

I’d like to ask this information to be made public, for the sake of us, framework developers. That’s not a breaking change, and that’s not a feature which can be abused. It’s just a way to issue a warning when a completed Future is not acceptable.

Author: Fantashit

1 thought on “Legitimate use case to detect if a Future is completed.

  1. A listener on a future is always notified in a later microtask than the one where it was registered.
    It may or may not be notified in the same microtask where the future completes (a sync completer can complete it immediately, an async one will do so in a later microtask). The future returned by an async function is currently completed synchronously if the function returns a value, at least if that happens after the first await – if there is no previous await, then the completion is actually delayed. Also, there is no promise that the future is completed synchronously, so depending on it is not something I recommend.

    Futures are asynchronous. Relying on them for synchronous communication is not a good idea.

    Example:

    import "dart:async";
    main() async {
      f() async {
        await 0;
        scheduleMicrotask(() {
          print("mt");  // prints second
        });
        return "ar";  // prints first
      }
      f().then(print);
      // Flush microtask queue.
      await new Future(()=>0);
      g() async {
        scheduleMicrotask(() {
          print("mt2");  // prints third
        });
        return "ar2";  // prints fourth
      }
      g().then(print);
    }

    So you are saying that you depend on futures being completed synchronously, which is not something the Future class actually promises. There are many situations where it’s not true, even for downstream handlers of a sync completer future (for example if one future handler throws, then all other completed futures are postponed to a later microtask).

    As for _isComplete (and getting the value if the future is complete with a value), it is very deliberate that it is not available in the Future API. The incentives are such that many, many users would be tempted to use it. We really, really don’t want that. Not only does it make the class harder to use properly (you shouldn’t use isComplete and value, but the incentives are wrong for that), it also makes people more dependent on the precise timing. Some would start assuming that the future is complete at some point, because it almost always is, except that one time where something got postponed to a later microtask for some unrelated reason, and the code breaks.

    Even if we wanted to add something to Future, it’s not practically possible. There are many, many implementations of Future in the wild, some mocks, some wrappers, some I have no idea what does. If we add anything to the Future interface, all those classes will become invalid. We can’t do that.

    The one way to access a future’s value is to add a listener and wait for the value. That value may come at any later time, and assuming differntly is unsound. That is the provided API.

    What you can do, if you really want a future with a synchronous access to the value, is to wrap the future:

    import "package:async/async.dart";
    class FutureValue<T> implements Future<T> {
      Future<T> _future;
      Result<T> _result;
      FutureValue(Future<T> future) : _future = future {
        Result.capture(_future).then((result) { _result = result; });
      }
      bool get isComplete => _result != null;
      T get value => _result.asValue?.value ?? (throw _result.asError.error); 
      // Forward all future functions
      R then<R>(FutureOr<R> action(T value), {Function onError}) => 
          _future.then<R>(action, onError: onError);
      ...
    }

    If you wrap the Future immediately you get it, then it will complete as you expect. You can’t do this for other people, but you can make sure the futures you return are wrapped.

    Or, if you don’t want to wrap the future, you can register it instead:

    class FutureValueRegister<T> {
      Expando<Result<T>> _registry = Expando();
      void register(Future<T> future) {
        Result.capture(future).then((result) { _registry[future] = result; } );
      }
      Result<T> resultOf(Future<T> future) => _registry[future];
    }

    Then you can always check whether a registered future has completed yet, and with what.

    (There have been previous requests for something like this, e.g., #29026)

Comments are closed.