Skip to content

proposal: avoid_futureor_void #59232

@eernstg

Description

@eernstg

avoid_futureor_void

Description

Flag the type FutureOr<void> as questionable.

Details

The type FutureOr<void> is inherently a contradictory type.

It is essentially the union of Future<void> and void. However, void is a top type so it is never possible to discriminate the union: Any object whatsoever with static type FutureOr<void> can be intended as the void case, in which case the object should be ignored and discarded.

The other interpretation (which might always be wrong) may occur if we actually have a Future<void> (that is, a Future<T> for any type T). In this case the object should not be ignored. We might want to await it (or pass it on to some other location where it will be handled), or we might want to .ignore() it in order to handle the situation where it is completed with an error, but we should do something (cf., for example, https://dart.dev/tools/linter-rules/unawaited_futures and https://dart.dev/tools/linter-rules/discarded_futures).

This means that any expression whose type is FutureOr<void> is a dilemma, and, as far as possible, we shouldn't have expressions with that type.

The remedy depends on the situation: For return types, it might be possible to replace the type FutureOr<void> by Future<void>?. Future<void>? can be discriminated precisely: If we received null then we know that there is nothing to handle. Otherwise we would have received a Future<void>; the future should be awaited, but result that it is completed with should be discarded (or we could .ignore() the future, but that's in itself a way to handle it). The dilemma is gone!

For parameter types, FutureOr<void> can be replaced by void: This means that the function body will not use that argument (unless it goes to great lengths to cheat us), and the type of the function will remain unchanged (according to the subtype relationships), so it won't break any clients or overrides.

If the function might actually use the future (if any), it should be considered whether the parameter type can be Future<void>?. This is again a clear type because we can discriminate it precisely in the function body. This may be more difficult to do, because the change from parameter type FutureOr<void> to parameter type Future<void>? makes the function a supertype of what it was previously, which is a breaking change in many ways. With the old type we could pass anything whatsoever, with the new type we can only pass null or a future. However, this might arguably be a better type for that function, because it is a less ambiguous characterization of the intended use of that parameter.

Kind

Guard against logical errors where a future is ignored, but should be handled, or vice versa.

Bad Examples

FutureOr<void> f() {...}
SomeType g(FutureOr<void> arg) {...} // `arg` not used.
SomeType h(FutureOr<void> arg) {...} // `arg` used in some cases.

Good Examples

Future<void>? f() {...}
SomeType g(void _) {...} // Non-breaking change for callers of `g` above.
SomeType h(Future<void>? maybeFuture) {...} // Breaking change from `h` above.

Discussion

There may be situations where a legitimate signature using type variables yields the type FutureOr<void> somewhere, because a type argument turns out to have the value void. For instance, a visitor may yield a result, but some visitors exist only because of their side effects, so they could have Visitor<void> as a supertype, and they might have a resulting member signature where FutureOr<void> occurs.

This is probably not easy to avoid, but it might still be helpful to mark those occurrences as special (by ignoring the lint), and comment on the right interpretation for that particular part of the interface.

Discussion checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions