Repository navigation
iOS: SentryEvent.Exception is null for native crashes, so a BeforeSend callback that dereferences it throws #5620
Description
Summary
On iOS, a user's managed BeforeSend callback is invoked for native crashes, and on that path SentryEvent.Exception is always null. A callback that dereferences Exception — entirely correct on every other path — throws a NullReferenceException for native crashes only.
This needs investigating rather than an obvious fix, hence a separate issue. Found while reviewing #5610, which changes what happens after the callback throws but does not introduce the null.
Why the callback runs at all
We deliberately hook the Cocoa SDK's own BeforeSend so that managed event processors and the user's BeforeSend also apply to native events — otherwise a native crash would bypass both:
| nativeOptions.BeforeSend = evt => ProcessOnBeforeSend(options, evt)!; |
ProcessOnBeforeSend converts the native event into a managed SentryEvent, runs managed processors, then calls SentryEventHelper.DoBeforeSend.
Why Exception is null
The conversion populates Exception only from an NSError:
| var exception = sentryEvent.Error == null ? null : new NSErrorException(sentryEvent.Error); |
var exception = sentryEvent.Error == null ? null : new NSErrorException(sentryEvent.Error);A signal or Mach-exception crash (SIGSEGV, EXC_BAD_ACCESS, …) carries no NSError, so Error is null and Exception stays null. The native crash detail is still present on the event — it's in SentryExceptions from the deserialized wire-format payload — it just never reaches the Exception property.
The path
flowchart TD
A["Native crash<br/><i>SIGSEGV, no NSError</i>"] --> B["Crash report written to disk<br/><i>process is dead</i>"]
B --> C["Next app launch<br/><i>Cocoa SDK reads the report</i>"]
C --> D["ToSentryEvent()<br/><i>Exception stays null</i>"]
D --> E["User BeforeSend runs<br/><i>e.Exception.Message</i>"]
E --> F["NullReferenceException"]
F --> G["Before #5610<br/><i>crash still reported,<br/>breadcrumb attached</i>"]
F --> H["After #5610<br/><i>crash report dropped</i>"]
Why it's easy to miss
Exception is populated on every managed path, so if (e.Exception.Message.Contains("token")) is correct everywhere an author would test it, and throws only here. Because it throws only here, managed errors keep arriving normally and just the native crashes are affected — the symptom is "we stopped getting iOS crash reports", with nothing pointing at BeforeSend.
Relationship to #5610
#5610 aligns BeforeSend failures with the callback error isolation spec (#5535): the item is dropped rather than sent with the exception stapled on as a breadcrumb. That is the right behaviour in general, and on this path it turns a degraded crash report into no crash report. It is logged at error level and counted in client reports, so it isn't silent, but neither is visible unless you go looking.
So #5610 changes the consequence; this issue is about the cause. ProcessOnBeforeSend gained test coverage for the throwing-callback case in #5610 (test/Sentry.Tests/Platforms/iOS/SentrySdkTests.cs).
Worth deciding
- Should
Exceptionbe populated for native crashes — e.g. synthesised fromSentryExceptions— or is null correct because there is no managed exception object? - If null is correct, is it documented anywhere a user writing
BeforeSendwould see it?SetBeforeSend's XML docs don't mention that the callback also runs for native events. - Does the same gap exist on the Android bridge, where
Native.EnableBeforeSendroutes native events through the managed callback too?
Activity
linear-code commentedon Sep 23, 2026
NET-822