AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

.NET MAUI Crash Debugging: Android & iOS Guide

NFNourin Mahfuj Finick··8 min read

.NET MAUI crash debugging is one of the least documented corners of cross-platform mobile development. When a MAUI app dies on a user's phone, the stack trace rarely tells the whole story: a managed C# exception can surface as a native SIGABRT on Android and as an Objective-C exception on iOS, and the code that actually crashed is often one or two frames away from the exception you logged. This guide walks through how crashes originate in .NET MAUI, how to catch them before they kill the app, and how to trace them to a fix across both Android and iOS.

Why .NET MAUI Crashes Look Different

.NET MAUI sits on top of two very different runtimes. On Android, your C# runs in the Mono runtime inside the Android Runtime (ART), so an unhandled exception is marshaled into a Java exception. On iOS, the same C# runs inside Mono's ahead-of-time compiled image, and unhandled exceptions are translated into Objective-C exceptions through the Objective-C runtime. That translation layer is where most MAUI crashes get confusing: a NullReferenceException in C# shows up in your crash dashboard as a SIGABRT with an Objective-C or Java frame at the top, not the C# frame you actually care about.

The practical consequence is that you cannot rely on default crash reporter behavior. Unhandled exceptions that reach the platform boundary are fatal by default and will terminate the app unless you subscribe to the platform's unhandled-exception hooks, as documented in the Microsoft .NET MAUI documentation. Your first job is to wire up global handlers on both platforms before a single crash ships to production.

Understanding this two-runtime reality is the foundation of effective MAUI crash debugging. The same C# exception behaves differently depending on whether it fires on the main UI thread, inside a background task, or within a native callback, so a single one-size-fits-all handler is never enough.

Catch Every Exception Before It Escapes

In .NET there are three global catch points you should register in MauiProgram.CreateMauiApp(): AppDomain.CurrentDomain.UnhandledException, which fires when an exception escapes on any thread; TaskScheduler.UnobservedTaskException, which fires when a faulted Task is garbage-collected without being observed; and the platform-specific hooks — AndroidEnvironment.UnhandledExceptionRaiser on Android and ObjCRuntime.Runtime.MarshalManagedException on iOS.

AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
    var ex = args.ExceptionObject as Exception;
    // Log the exception and its inner exceptions, then decide whether to bail.
};
 
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
    args.SetObserved(); // prevent the fault from tearing the process down
    args.Exception.Handle(ex => { /* log */ return true; });
};

The subtle part is that AppDomain.UnhandledException is largely informational on modern .NET: by the time it fires, the process is already on its way down. The AppDomain.UnhandledException documentation makes this explicit — use it to log a final breadcrumb and flush your crash reporter, not to recover. The platform hooks below, by contrast, let you intercept the crash before it becomes fatal, which is why they matter more for MAUI. This defense-in-depth approach — intercepting at the platform boundary while isolating failures at the component level — mirrors the pattern we document in mobile app crash isolation: error boundary patterns.

Android: The UnhandledExceptionRaiser Path

On Android, MAUI surfaces unhandled managed exceptions through AndroidEnvironment.UnhandledExceptionRaiser. Subscribe to it in MauiProgram and you receive a RaiseThrowableEventArgs whose Exception is a Java.Lang.Throwable wrapping your C# exception. This is the moment to record the managed stack, attach context, and decide whether to set Handled to true.

AndroidEnvironment.UnhandledExceptionRaiser += (sender, args) =>
{
    var throwable = args.Exception; // Java.Lang.Throwable
    // args.Handled = true; // only if you can guarantee a safe recovery
};

Two Android-specific traps catch MAUI developers out regularly. The first is a Java.Lang.RuntimeException wrapping a ClassNotFoundException or MethodNotFoundException, which is almost always a trimming or AOT issue in the release build. The second is a SIGSEGV from JNI interop when a managed object is collected while a native callback still holds a reference to it. Keep a strong reference to any object you pass across the interop boundary, and review your AOT profile settings before blaming the framework. Because MAUI release builds for Android rely on trimming and ahead-of-time compilation, methods that are reachable in debug can be stripped or not compiled in release, which is why these failures often appear only on production devices. The hook is documented by Microsoft at AndroidEnvironment.UnhandledExceptionRaiser.

iOS: MarshalManagedException and ObjC Interop

On iOS the bridge works in the other direction: a managed exception thrown during an Objective-C callback is converted by ObjCRuntime.Runtime.MarshalManagedException into an ObjCException that the Objective-C runtime sees as an unhandled NSException. When that happens you get the classic iOS crash signature — a SIGABRT with __cxa_throw and objc_exception_throw at the top of a native stack that tells you almost nothing about your C#.

ObjCRuntime.Runtime.MarshalManagedException += (sender, args) =>
{
    var ex = args.Exception; // the managed exception
    // Record it before the ObjC runtime tears the process down
};

The key iOS discipline is to never let an exception cross an async callback boundary. async void event handlers and fire-and-forget tasks on the main thread are the two biggest sources of MarshalManagedException crashes in MAUI. The fix is usually to convert fire-and-forget calls into properly awaited tasks and to wrap event handlers in try/catch blocks so exceptions never escape into the Objective-C runtime in the first place. The hook itself is documented at ObjCRuntime.Runtime.MarshalManagedException.

Symbolication: dSYM and PDB Files

A native crash report is useless until it is symbolicated. On iOS, that means uploading your .dSYM files — MAUI emits them for the native portions of your app, while your managed stack is resolved separately from the portable .pdb files. On Android, the native libmonodroid and libmonosgen symbols come from the .so files in your release build, and the managed frames are resolved against the .pdb. Without these artifacts, your crash report shows raw memory addresses instead of the method names and line numbers you need to fix the bug.

Enable managed debugging symbols in release builds with:

<PropertyGroup>
  <DebugType>portable</DebugType>
  <DebugSymbols>true</DebugSymbols>
  <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>

On iOS, add --dsym to your mtouch arguments or set MtouchExtraArgs so the .dSYM bundle is produced alongside the .ipa. If your crash reporter shows native frames but no C# frames, you are almost always missing the matching dSYM or PDB. We walk through the full symbolication workflow — including ProGuard and R8 mapping on Android — in our mobile crash stack trace symbolication guide.

Diagnose with dotnet-trace and dotnet-dump

For crashes that refuse to reproduce in the debugger, .NET's diagnostic tools give you a production-grade path in. dotnet-trace captures a runtime trace you can analyze for exceptions, and dotnet-dump grabs a full process memory dump you can inspect offline.

dotnet-trace collect --process-id <pid> --duration 00:00:30
 
dotnet-dump collect --process-id <pid>
dotnet-dump analyze <dump>.dmp

Inside dotnet-dump analyze, the clrstack, pe, and dumpexceptions commands let you walk the managed stack and inspect exception objects directly — even on an AOT-compiled iOS build, where the managed heap is still fully introspectable. Together these tools turn a crash that only happens in production into something you can reconstruct locally, which is especially valuable for interop failures where the exception is only visible on one platform. The tools are documented at dotnet-trace and dotnet-dump.

Plug MAUI Crashes into BugsPulse

All of the above is detection and diagnosis; you still need a place where every exception lands with full context. BugsPulse gives you a privacy-first home for MAUI crash data: breadcrumbs around the moment of the crash, release and device segmentation, and the ability to correlate a managed exception with the native frame it surfaced through, so the SIGABRT you see in the dashboard actually maps back to the C# line that threw.

Because MAUI shares much of its architecture with Xamarin.Forms and the broader .NET ecosystem, the correlation patterns are the same ones we describe in cross-stack crash correlation for mobile apps. Wire the BugsPulse SDK into the same global handlers from the sections above, and every exception — managed, native, or interop — lands in one timeline. Try BugsPulse to see your MAUI crashes with full managed and native context.

Migrating from Xamarin.Forms: The Hidden Crash Sources

A large share of MAUI apps in 2026 are Xamarin.Forms migrations, and migration is where a specific class of crash hides. Three recurring ones:

  1. Renderer-to-handler mismatches. Xamarin custom renderers do not map one-to-one to MAUI handlers. A renderer that mutated a native view directly can crash under MAUI's handler lifecycle if the platform view is recreated.
  2. DependencyService timing. Code that resolved DependencyService lazily in Xamarin can throw NullReferenceException in MAUI if the registration happens after first use.
  3. Android AOT profile drift. Xamarin apps shipped with a custom AOT profile; MAUI's defaults differ, and methods that compiled before can now hit MethodNotFoundException under trimming.

Microsoft's migration guide flags several of these breaking changes explicitly — see Migrate from Xamarin.Forms to .NET MAUI. The single highest-value move is to run the .NET upgrade assistant and then do a full crash audit on a real device before you ship the migrated build, watching the handlers above for anything new.

Ship Crash-Free MAUI Builds

.NET MAUI crash debugging comes down to a consistent loop: register every global exception hook early, symbolicate your builds so native frames resolve to C#, use dotnet-trace and dotnet-dump when a crash will not reproduce, and route everything into a single crash timeline where managed and native frames sit side by side. The framework may be newer than Xamarin.Forms, but the discipline is the same one that keeps any cross-platform app healthy: catch what you can, symbolize what you can't, and never let an exception cross a platform boundary silently.

Start debugging your MAUI crashes today — create a free BugsPulse account and get your first managed-plus-native crash timeline in minutes.