dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.06k stars 1.56k forks source link

Unhandled exception thrown by stream after a delay #52939

Closed lrampazzo closed 1 year ago

lrampazzo commented 1 year ago

Hi!

I am experiencing an issue where this code succeeds but throws also another unhandled FormatException which keeps execution running:

void main() async {
  Stream<int> operation() async* {
    yield 1;
    throw const FormatException();
  }

  final future = operation().last;
  try {
    // removing this will cause the unhandled exception to not occur
    await Future.delayed(Duration.zero);
    await future;

    print("fail");
  } on FormatException {
    print("ok");
  }
}

By commenting the Future.delayed (and avoiding event loop rescheduling), the exception is handled correctly.

It also happens by implementing the same as a Future and with StreamController, Stream.multi, etc..

Issue is also reproducible on DartPad

Dart SDK version: 3.0.5 (stable) (Mon Jun 12 18:31:49 2023 +0000) on "macos_arm64" Flutter (Channel stable, 3.10.5, on macOS 13.4.1 22F770820d darwin-arm64, locale en-IT) • Flutter version 3.10.5 on channel stable • Upstream repository https://github.com/flutter/flutter.git • Framework revision 796c8ef792 (4 weeks ago), 2023-06-13 15:51:02 -0700 • Engine revision 45f6e00911 • Dart version 3.0.5 • DevTools version 2.23.

lrhn commented 1 year ago

This is working as intended.

The call to last starts listening on the stream, for any events until the stream closes. And when the stream computation throws, the future returned by last completes with that error.

As for any other future, there must be a listener on the future when it completes with an error, otherwise the error will be considered, and reported as, unhandeled.

You don't add a listener to the future here until you await it, and awaiting the delayed future first makes that be to late.

What you can do is to call operation.ignore() first. That tells the future that you don't care if it completes with an error. You can still await it later, and get the error there, it will just not be reported as unhandeled.

lrampazzo commented 1 year ago

Thanks @lrhn for the clarification.