setExceptionPauseMode causes a breakpoint to be triggered with async exceptions that are caught
The VM Service Protocol provides the setExceptionPauseMode API. This API is used by debugging tools like the VSCode Dart Plugin or the IntelliJ Dart plugin when debugging, where the following RPC is sent over from the plugin:
==> {"id":"8","jsonrpc":"2.0","method":"setExceptionPauseMode","params":{"isolateId":"isolates/2741664618071259","mode":"Unhandled"}}
When this happens, uncaught exceptions at runtime will surface as breakpoints:
<== {"jsonrpc":"2.0","method":"streamNotify","params":{"streamId":"Debug","event":{"type":"Event","kind":"PauseException","isolate": <truncated>
This has the same effect as the set break-on-exception Unhandled command in the debugger, and the --pause-isolates-on-unhandled-exceptions flag to dart.
Problem
With the following code, exceptions that are caught will surface in the debugger. Consider this:
import 'package:test/test.dart';
void main() {
test('foo', () async {
try {
// Inlining [throwFooException] here also makes the problem go away.
await throwFooException();
} on FooException {
print('caught fooexception');
}
});
}
class FooException implements Exception {}
Future<void> throwFooException() async {
// Commenting this out makes the problem go away.
// Wait for the next event.
await Future(() {});
throw FooException();
}
Running it through the package:test executable, or with dart directly will cause a breakpoint to be added where that exception is thrown, with an awkward backtrace coming from StackZoneSpecification.
With the Test Executable:
pub run test --pause-after-load- Go to the observatory debugger, and enter
set break-on-exception Unhandledfollowed bycontinue - Observe that a breakpoint has been added where we
throw FooException()
Running it with pub run test --pause-after-load --no-chain-stack-traces and repeating the process does not cause the same issue though. The issue is with Chain.capture used to wrap user defined tests here in package:test_api.
With dart
dart run --pause-isolates-on-start --pause-isolates-on-unhandled-exceptions --enable-vm-service bin/dart_debugger_uncaught_exception_repro.dart- Go to the observatory debugger, and enter
continue - Observe that a breakpoint has been added where we
throw FooException()
Context
This particular pattern is used by, but is not limited to the package:integration_test binding under the hood. A MissingPluginException may be thrown and caught, but it will surface during IDE debugging which is confusing to users.
(cc @DanTup)
Smaller Repro
import 'package:stack_trace/stack_trace.dart';
class FooException implements Exception {}
Future<void> throwFooException() async {
// Commenting it out makes the problem go away.
// Wait for the next event.
await Future(() {});
throw FooException();
}
void main() {
Chain.capture(() async {
try {
// Inlining [throwFooException] here also makes the problem go away.
await throwFooException();
} on FooException {
print('caught fooexception');
}
});
}
Where the same command of dart run --pause-isolates-on-start --pause-isolates-on-unhandled-exceptions --enable-vm-service bin/dart_debugger_uncaught_exception_repro.dart is able to reproduce it.
What seems to help is when this particular line is commented out. I couldn't quite follow what is going on with the recursion though, so I'm not sure what is the exact cause of this.
Versions
Dart SDK version: 2.13.0-38.0.dev (dev) (Mon Feb 15 10:21:50 2021 -0800) on "macos_x64"
stack_trace: 1.10.0