stack_trace icon indicating copy to clipboard operation
stack_trace copied to clipboard

setExceptionPauseMode causes a breakpoint to be triggered with async exceptions that are caught

Open jiahaog opened this issue 4 years ago • 0 comments

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:

  1. pub run test --pause-after-load
  2. Go to the observatory debugger, and enter set break-on-exception Unhandled followed by continue
  3. 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

  1. dart run --pause-isolates-on-start --pause-isolates-on-unhandled-exceptions --enable-vm-service bin/dart_debugger_uncaught_exception_repro.dart
  2. Go to the observatory debugger, and enter continue
  3. 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

jiahaog avatar Feb 19 '21 09:02 jiahaog