With debugger set to pause on uncaught exceptions, it (correctly) breaks on this:
main() {
throw new Exception();
}
However, if the method is marked async, it does not:
main() async {
throw new Exception();
}
I understand async changes things significantly, but I would've still expected this to work (currently it dumps the exception output and terminates).
The issue here is any exception thrown in an async method is always caught, just as any exception thrown in the then handler of a future is caught.
Consider
fail() async => throw new Exception();
yeild_to_event_loop() async => null;
main() async {
var f = fail();
await yeild_to_event_loop();
// f has completed with an error
try {
await f;
} catch (e) {
print("Handled $e");
}
}
At the time the exception is first thrown, the future for fail's invocation has no listeners, but the exception has been caught by the future implementation and it will later be rethrown and handled in main.
I've done some more testing on this; and the issue is not isolated to async methods but just async code in general (I guess this is what you meant).
This seems really crazy to me; it seems like async makes "break on unhandled exceptions" non-functional and therefore synchronous code becomes a much easier way to code and debug.
I know C# is a different beast (for ex doesn't support async mains so you need to manually a block on async code at the top level) but debugging async code works pretty much like sync code. When an exception occurs (and gets back to something that was waiting for the response) the debugger breaks, jumps to the location where the exception occurred and even has functional watch/evals!
It seems like Dart has the necessary info to do the same - presumably there's something outside of main that's waiting for futures to complete (else the program would immediately terminate). When a future throws it just dumps the exception to the console; why should it not break the debugger?
Take this Dart program:
main() {
doStuff();
}
Future doStuff() {
return new Future.delayed(new Duration(seconds: 1)).then((_) {
throw new Exception();
});
}
It waits 1 second, then throws. If I run it under the debugger, it never breaks, but dumps the exception to the console:
dart danny.dart
Unhandled exception:
Exception
#0 doStuff.<anonymous closure> (file:///c:/Users/danny/Desktop/DartSample/danny.dart:19:5)
#1 _RootZone.runUnary (dart:async/zone.dart:1404)
#2 _FutureListener.handleValue (dart:async/future_impl.dart:131)
#3 _Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:637)
#4 _Future._propagateToListeners (dart:async/future_impl.dart:667)
#5 _Future._complete (dart:async/future_impl.dart:467)
#6 Future.Future.delayed.<anonymous closure> (dart:async/future.dart:228)
#7 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:16)
#8 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:385)
#9 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:414)
#10 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:148)
finished (255)
Here's the closest equiv I can make in C#:
class Program
{
static void Main(string[] args)
{
DoStuff().Wait();
}
static Task DoStuff()
{
return Task.Delay(1000).ContinueWith((_) => { throw new Exception(); });
}
}
Note that I have to force blocking in Main by calling .Wait() because C# doesn't support an async main (for Dart, it seems that this code is effectively handled outside of main by the VM, but the idea is the same).
When I run that in C#, here's what I get:

The debugger breaks at the location of the exception and all the usual functionality (watch, evaluation etc.) works.
I'm sure implementing was no mean feat, but it seems like this should the _desired_ behaviour?
When executing async code, the VM does indeed not know whether an exception will be handled or not. At best, it could halt on any exception that is thrown, whether it's handled or not.
The VM looks at the execution stack to determine whether there is an exception handler that catches and exception. When async code is executed, there is an implicit catch-all clause on the stack that will catch any exception and forward it to the Future object. The VM cannot know whether the Future handles the exception.
Thus, from the VM's perspective, every exception is caught in async code.
@mhausner I understand; I'm not expecting the VM to be psychic, but I would expect it to work like C#.
At the point where something waits for the future to complete (which in C# is when you call .Wait() or access .Result and cause a block, but in Dart is just outside of main) if it completes with an exception, the debugger should pause and jump to the location where it occurred (and ideally, provide all of the state to allow examining variables, etc.).
If C# can do this, why can't Dart? Without it, I fear that async code is just going to be really annoying to debug and sync code will have an advantage.
I imagine to implement this there would have to be some coordination with the dart:async library (and the implementation of Future) and the dart:developer library. When a Future was about to return an uncaught exception, it would check if a debugger was attached to the isolate, and the current break-on-exception setting, and issue some sort of break on exception call via dart:debugger.
You'd probably need to have some support for returning information about async frames in order to make this useful (like what package:stack_trace does). This is related: https://youtrack.jetbrains.com/issue/WEB-15765
The information whether or not an exception will be caught by user-defined code is simply not on the stack when async code is involved. The stack only contains the call chain from the innermost function to the innermost async function. user-defined catch handlers are not on the stack until the Future is completed, at which time the activation frame that created the exception is no longer available.
Similary, by the time a Future returns an uncaught exception (aka completes with error), the stack has already been unwound, i.e. the activation frame that threw the exception is no longer on the stack. Yes, you can pause the debugger at that time, but you can't inspect the local variables of the call chain.
I don't know the details of how C# works, but I believe an exception in async code only breaks the debugger at the time something tries to resolve the task (eg. wait for its completion). Waiting for (or accessing the result of) a faulted task throws the original exception; however the debugger takes you to the location of the exception and _not_ where the call to Wait()/.Result was.
I don't know if there are semantic differences between C# and Dart async, but if not, it seems like this same thing should be possible (though I'm certain it's not simple; breaking the debugger back in a method that has basically already run and thrown!).
@mhausner We need to look at the chain of futures that the error is going to be sent down and figure out if any user code has registered a catchError handler. This is analogous to locating the catch block on the stack.
John has been working on this recently.
I have a patch now that computes the "awaiter return call stack". This will let us determine if an async function has the await statement inside of a try block. It won't be perfect (e.g. it won't handle the case of a an async function's Future with a .catchError on it) but it's a start. The root issue here is that Dart's original asynchronous primitives Future, Completer, and Stream are not designed in such a way that debugging is very feasible.
In the near future the VM will do a reasonable job for programs that use async and await keywords without using .then, etc..
Good to hear there's been progress!
I'm so used to this in C# it didn't occur to me it might be complicated to implement; I hope others see a benefit in this too so it's not just a drain on resources for moi!
(I was actually talking to a colleague about this today and he was annoyed be it too, so I guess there's at least two of us! 馃槈)
Any progress here?