Handling errors in void Dart function marked async

67 views Asked by At

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()?

2

There are 2 answers

0
jamesdlin On BEST ANSWER

What is going on?

Functions marked with async are automatically transformed so that return values and thrown objects are wrapped in Futures. Failed Futures 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();
  ...
}
2
pixel On

See, i can explain about the difference in your expected result and actual result.

Look when you mark a function as async, it means that the function returns a special type of object called a Future. A Future represents a potentially asynchronous operation that may or may not be completed. When you call an async function, it returns immediately, and the work inside the function happens in the background. This is what enables Dart to perform asynchronous programming.

And in your test function, you are taking another function called run as an argument. When you mark run as async, it means it's returning a Future<void>, and when you call it without await, the function execution is not paused to wait for the run function to complete. Instead, it continues with the loop immediately.

This is why you see the loop running without waiting for the run function to finish. It's running concurrently, and the exceptions it throws aren't caught in the try/catch block because the try/catch block is already completed before the run function finishes.

And the Code below will lead to your expected answer:

import 'dart:async';

Future<void> test(Future<void> Function() run) async {
  try {
    for (var i = 0; i < 3; i++) {
      print(i);
      await run(); // Await the execution of the async function.
    }
  } catch (e) {
    print(e);
  } finally {
    print('In finally block.');
  }
}

Future<void> main(List<String> args) async {
  await test(() async {
    throw 'In void function erroneously marked async';
  });
}

Now, think how it's give expected result! reply if you don't get.