Skip to content

runZonedGuarded error handler is async, causing errors to be silently swallowed #3541

Description

@buenaflor

Description

The sentryOnError handler in SentryRunZonedGuarded.sentryRunZonedGuarded is declared as async, but Dart's runZonedGuarded expects a synchronous void Function(Object, StackTrace) callback.

File: lib/src/sentry_run_zoned_guarded.dart:19

final sentryOnError = (exception, stackTrace) async {
  await _captureError(hub, options, exception, stackTrace);
  if (onError != null) {
    onError(exception, stackTrace);
  }
};

This causes two problems:

1. The onError callback runs inside an unawaited Future

Since sentryOnError is async, it returns a Future<void> which Dart silently coerces to void. The runZonedGuarded implementation never awaits this future. This means:

  • _captureError may not complete before the process exits — events can be lost
  • The user's onError callback executes inside the unawaited future rather than synchronously

2. Rethrowing in onError creates an infinite loop that Dart silently drops

If the user's onError callback rethrows the error (a common pattern to preserve normal crash behavior):

Sentry.runZonedGuarded(() async {
  await Sentry.init(...);
}, (error, stackTrace) {
  throw error; // intended to let the error propagate
});

The throw error happens inside the unawaited async future, which makes it a new uncaught error in the same zone — triggering sentryOnError again in an infinite loop. Dart breaks this loop by silently dropping the error.

Net effect: The unhandled exception is completely swallowed — no stack trace is printed, and the Sentry event may not be sent either.

Expected behavior

Errors should propagate normally after being captured by Sentry. The user's onError callback should be able to rethrow errors to preserve the default unhandled exception output.

Reproduction

Future<void> main() async {
  Sentry.runZonedGuarded(() async {
    await Sentry.init(
      (options) {
        options.dsn = '...';
      },
      callAppRunnerInRunZonedGuarded: false,
      appRunner: () async {
        throw Exception('some test');
      },
    );
  }, (error, stackTrace) {
    throw error;
  });
}

Without Sentry: prints Unhandled exception: Exception: some test with full stack trace.
With Sentry: silently exits with no output.

Suggested fix

Make sentryOnError synchronous and call _captureError without awaiting (or use unawaited), then invoke the user's onError synchronously:

final sentryOnError = (exception, stackTrace) {
  unawaited(_captureError(hub, options, exception, stackTrace));
  if (onError != null) {
    onError(exception, stackTrace);
  }
};

This preserves the synchronous contract expected by runZonedGuarded and allows the user's onError to rethrow normally.

Metadata

Metadata

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions