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.
Description
The
sentryOnErrorhandler inSentryRunZonedGuarded.sentryRunZonedGuardedis declared asasync, but Dart'srunZonedGuardedexpects a synchronousvoid Function(Object, StackTrace)callback.File:
lib/src/sentry_run_zoned_guarded.dart:19This causes two problems:
1. The
onErrorcallback runs inside an unawaited FutureSince
sentryOnErrorisasync, it returns aFuture<void>which Dart silently coerces tovoid. TherunZonedGuardedimplementation never awaits this future. This means:_captureErrormay not complete before the process exits — events can be lostonErrorcallback executes inside the unawaited future rather than synchronously2. Rethrowing in
onErrorcreates an infinite loop that Dart silently dropsIf the user's
onErrorcallback rethrows the error (a common pattern to preserve normal crash behavior):The
throw errorhappens inside the unawaitedasyncfuture, which makes it a new uncaught error in the same zone — triggeringsentryOnErroragain 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
onErrorcallback should be able to rethrow errors to preserve the default unhandled exception output.Reproduction
Without Sentry: prints
Unhandled exception: Exception: some testwith full stack trace.With Sentry: silently exits with no output.
Suggested fix
Make
sentryOnErrorsynchronous and call_captureErrorwithout awaiting (or useunawaited), then invoke the user'sonErrorsynchronously:This preserves the synchronous contract expected by
runZonedGuardedand allows the user'sonErrorto rethrow normally.