Home > front end >  Dart async*, yield* and exception handling
Dart async*, yield* and exception handling

Time:11-17

If I have nested async* streams, exceptions do not appear to be catchable, which is counterintuitive.

An example:

void main() {
  getString().listen(print);
}

Stream<String> getString() async* {
  try {
    yield* asyncStarError();
    yield await asyncError();
  } catch (err) {
    yield 'Crash';
  }
}

Stream<String> asyncStarError() async* {
  throw Exception('A Stream error happened');
}

Future<String> asyncError() async {
  throw Exception('A Future error happened');
}

This outputs:

Uncaught Error: Exception: A Stream error happened
Crash

So the exception thrown by asyncStarError is not caught, while the Future is caught as expected. Can anyone explain why?

You can observe the behaviour in dartpad: https://dartpad.dartlang.org

CodePudding user response:

The yield* forwards all events from the stream it yields, including errors. So, asyncStarError() produces a stream with an error event, and yield* forwards that error to the stream returned by getString, and then the getString.listen(print); does not add a handler so the error becomes uncaught. (I'm guessing you're running this in a browser since that uncaught error doesn't crash the program.)

After that, the yield await asyncError() never yield anything. The await asyncError() itself throws, before reaching the yield, and that error is then caught by the catch which yields crash.

If you want to catch the errors of a stream, you need to actually look at the events, not just use yield* to forward them all blindly, including error events.

For example:

    await for (var _ in asyncStarError()) {
      // Wohoo, event!
    }

would make the error from the asyncStarError stream an error at the loop. It would then be caught and you'd print "Crash".

TL;DR: The yield* operation forwards error events, it doesn't raise them locally.

CodePudding user response:

The difference is that async generator (async*) exceptions cannot be caught by surrounding it with try/catch.

Instead, you should use the callback handleError to achieve the behaviour you are looking for.

To go further, you may be interested in using runZonedGuarded.

CodePudding user response:

Since yield * forwards them like @lrn said, you can catch them in main just fine. Or wherever you are consuming them.

void main() {
  getString().listen(print).onError((e) => print('error caught: $e'));
}

Stream<String> getString() async* {
  try {
    yield* asyncStarError();
    yield await asyncError();
  } catch (err) {
    yield 'Crash';
  }
}

Stream<String> asyncStarError() async* {
  throw Exception('A Stream error happened');
}

Future<String> asyncError() async {
  throw Exception('A Future error happened');
}

this prints:

error caught: Exception: A Stream error happened
Crash
  • Related