I am designing an API and would like to handle a situation when a user marks a void
function async
by mistake.
The simplified code is listed below:
void test(void Function() run) {
try {
for (var i = 0; i < 3; i++) {
print(i);
run();
}
} catch (e) {
print(e);
} finally {
print('in finally block.');
}
}
void errorThrower(String message) {
print('in errorThrower ');
throw message;
}
void main(List<String> args) {
test(() async{ // <-------- marked async by API user's mistake
print('in run ...');
errorThrower('error thrown in test');
});
}
If the function is not marked async
the program output is as expected and the error is
thrown, caught, handled, and the finally
block is executed:
$ dart main.dart
0
in run ...
in errorThrower
error thrown in test
in finally block.
However, if the function is marked async
the console output is:
$ dart main.dart
0
in run ...
in errorThrower
1
in run ...
in errorThrower
2
in run ...
in errorThrower
in finally block.
Unhandled exception:
error thrown in test
#0 errorThrower (file:///home/dan/WORK/DartProjects/benchmark_runner/bin/throws_test.dart:16:3)
#1 main.<anonymous closure> (file:///main.dart:22:5)
#2 test (file:///main.dart:5:10)
#3 main (file:///main.dart:20:3)
#4 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:294:33)
#5 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)
What is going on? I tried a debugger, the program steps into run()
then errorThrower
and then the loop continues.
It looks like an unawaited future error but I don't know how to catch and handle it since a void
function cannot be awaited.
Is there any way I can catch and handle the error without changing the signature o f run()
?
What is going on?
Functions marked with async
are automatically transformed so that return values and thrown objects are wrapped in Future
s. Failed Future
s must be handled by registering a callback with Future.catchError
, by using using await
on the Future
within a try
block (which is syntactic sugar for registering a Future.catchError
callback), or by setting up a Zone
error handler (which I'll consider to be outside the scope of this answer). Conceptually you can consider a failed Future
as throwing an exception from the Dart event loop, after your synchronous test
function has already left its try
-catch
block.
Is there any way I can catch and handle the error without changing the signature of
run()
?
A T Function()
is a subtype of (is substitutable for) void Function()
. Therefore at compile-time you cannot prevent a Future<void> Function()
from being passed where a void Function()
is expected. This is also why things like Iterable.forEach
can't prevent asynchronous callbacks (even though it's almost always a bad idea) and would need support from the linter.
You could add a runtime check:
void test(void Function() run) {
if (run is Future<void> Function()) {
throw ArgumentError("test: Cannot be used with asynchronous callbacks.");
}
...
}
Or if you just want to swallow the error:
void test(void Function() run) {
...
if (run is Future<void> Function()) {
run().catchError((e) => print(e));
} else {
run();
}
...
}
Note that if you want test
to wait for run
to complete (successfully or not), then test
itself would need to be asynchronous and would need to return a Future
. At that point, you might as well always assume that run
might be asynchronous and unconditionally await
it:
Future<void> test(FutureOr<void> Function() run) async {
try {
...
await run();
...
}