Repository: flutter
Issue: [PlatformChannel] Support Dart-side handling of errors/exceptions thrown during Dart-side method call handling
Labels: engine,c: proposal,team-ecosystem,P2,a: plugins,triaged-ecosystem

Internal: b/327780213

### Use case

Consider an unhandled `Exception` being thrown in a Flutter app's main Dart isolate. Typically, this `Exception` is expected to be handled by one or more of these three mechanisms:

1. [binding.platformDispatcher.onError](https://api.flutter.dev/flutter/dart-ui/PlatformDispatcher/onError.html)
2. [Isolate.current.addErrorListener](https://api.flutter.dev/flutter/dart-isolate/Isolate/addErrorListener.html)
3. [runZoneGuarded's onError](https://api.flutter.dev/flutter/dart-async/runZonedGuarded.html)

The following example app shows an exception that surprisingly isn't handled by any of the above mechanisms:

```dart
import 'dart:async';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() async {
  final binding = WidgetsFlutterBinding.ensureInitialized();
  binding.platformDispatcher.onError = (error, stackTrace) {
    // boom is not caught here.
    print('blah platformDispatcher got error $error');
    return true;
  };
  Isolate.current.addErrorListener(
    RawReceivePort((dynamic pair) {
      final errorAndStackTrace = (pair as List<Object>).cast<String>();
      // boom is not caught here.
      print('blah isolate $errorAndStackTrace');
    }).sendPort,
  );

  await runZonedGuarded(() async {
    runApp(Container(color: Colors.red));

    // Create the two method channels used in this demo.
    const ping = MethodChannel('ping');
    const pong = MethodChannel('pong');
    
    // Throw an exception when the platform calls the `pong` channel.
    pong.setMethodCallHandler((call) async {
      throw Exception('boom');
    });

    // Kick-off the `pong` call by calling the `ping` channel
    await ping.invokeMethod('');
  }, (error, st) {
    // boom is not caught here.
    print('blah runZoneGuarded $error, $st');
  });
```

Instead, the exception is forwarded to the platform for handling via the `MethodChannel`, example implementation for Android:

```java
package com.google.android.flutter.samples.minimal;

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;

public class MainActivity extends FlutterActivity {
  @Override
  public void configureFlutterEngine(FlutterEngine flutterEngine) {
    PluginRegistrant.registerWith(flutterEngine);
    var ping = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "ping");
    var pong = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "pong");

    ping.setMethodCallHandler(
        (call, result) -> {
          result.success(null);
          pong.invokeMethod(
              /* method */ "boom",
              /* arguments */ null,
              // Without this, the "boom" error in Dart will be silently dropped.
              /* callback */ new Result() {
                @Override
                public void success(Object result) {
                  android.util.Log.w("MinimalActivity", "blah success");
                }

                @Override
                public void error(String errorCode, String errorMessage, Object errorDetails) {
                  android.util.Log.w(
                      "MinimalActivity",
                      "blah error errorCode="
                          + errorCode
                          + " errorMessage="
                          + errorMessage
                          + " errorDetails="
                          + errorDetails);
                }

                @Override
                public void notImplemented() {
                  android.util.Log.w("MinimalActivity", "blah notimplemented");
                }
              });
        });
  }
}
```

### Proposal

Instead of always forwarding exceptions thrown in Dart method call handlers back to the platform, provide an otpional mechanism for those exceptions to escape and be handled in Dart.