-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Context
It is common that a method only supports synchronous execution (such as if it works by doing a try-finally). For example, State.initState
is one of those. It purposefully rejects:
@override
Future<void> initState() {...}
// ^ Will throw an assertion error
Similarly, a common design pattern is to rely on closures to execute code before/after. For example:
void fn() {
benchmark('fn', () {
});
}
...
T benchmark<T>(T Function() cb) {
final stopwatch = Stopwatch();
try {
stopwatch.start();
return cb();
} finally {
stopwatch.stop();
}
}
But such a benchmark
implementation doesn't handle functions that return a Stream/Future, as those are unawaited. To handle those, we often need a benchmarkAsync
variant:
Future<T> benchmarkAsync<T>(Future<T> Function() cb) {
final stopwatch = Stopwatch();
try {
stopwatch.start();
return await cb();
// ^ awaited now
} finally {
stopwatch.stop();
}
}
The problem... It is easy to use benchmark
instead of benchmarkAsync
by mistake.
You could write:
benchmark(() async {
await something();
}
Yet the future wouldn't be awaited.
benchmark
could, like State.initState
, include an assert to catch this mistake. But that's a runtime error, which is not cool!
Proposal: Add some form of annotation to reject Future/Stream
Could we maybe have a @avoidReturnAsync
or something similar, placed on a function declaration or parameter?
TL;DR consider:
class State {
@avoidReturnAsync
void initState() {}
}
T benchmark<T>(
@avoidReturnAsync
T Function() cb,
) {}
Then we'd have:
class A extends State {
@override
Future<void> initState() {} // KO
}
class A extends State {
@override
Stream<void> initState() {} // KO
}
...
benchmark(() => Future.value(0)); // KO
benchmark(() => Stream.value(0)); // KO
This would avoid unexpected asynchronous override when only a synchronous implementation is supported.