Reputation: 1371
Is there a way to set up a global error handler for a MAUI app? My app is currently running fine when I launch it from Visual Studio. However on all simulators if I try and start the app up from the shell's app icon, it starts up then quits (and on iOS I get a crash dump). I have no idea where this is happening at, so wanted to try and catch in a global handler.
I looked at builder.ConfigureMauiHandlers, but it appears to only work with controls. You can't use a type of UnhandledExceptionEventHandler, which is what I was thinking would be appropriate.
I also tried an AppDomain handler, but it doesn't not appear to work either:
AppDomain ad = AppDomain.CurrentDomain;
ad.UnhandledException += Ad_UnhandledException;
Is this possible? I've only found samples for WCF and they don't work.
Upvotes: 25
Views: 12302
Reputation: 25966
You can persist unhandled exceptions in Preferences with:
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
if (e.ExceptionObject is Exception ex)
{
Trace.WriteLine($"The app crashed on {DateTimeOffset.Now.LocalDateTime} with the following message:\n\n{ex.Message}\n\n{ex.StackTrace}");
Trace.Flush();
Preferences.Default.Set("LastCrashTime", DateTimeOffset.Now.ToUnixTimeMilliseconds());
Preferences.Default.Set("LastCrashMessage", ex.Message);
Preferences.Default.Set("LastCrashStackTrace", ex.StackTrace);
}
};
Then, on the next run of the app, you can casually retrieve any exceptions from the last 24 hours, and choose what to do with it.
if (Preferences.Default.Get("LastCrashTime", 0L) is long lastCrashTime
&& DateTimeOffset.Now.ToUnixTimeMilliseconds() < lastCrashTime + 24 * 60 * 60 * 1000
&& Preferences.Default.Get("LastCrashMessage", string.Empty) is string lastCrashMessage
&& Preferences.Default.Get("LastCrashStackTrace", string.Empty) is string lastCrashStackTrace)
{
Trace.WriteLine($"The app crashed on {DateTimeOffset.FromUnixTimeMilliseconds(lastCrashTime).LocalDateTime} with the following message:\n\n{lastCrashMessage}\n\n{lastCrashStackTrace}");
}
Upvotes: 0
Reputation: 1571
There is a pretty lengthy GitHub discussion here: Global exception handling #653
The most popular response is from one of the Sentry developers, who offered a gist solution using first chance exception.
The windows-specific implementation includes the following handlers, where _lastFirstChanceException
is a private static variable.
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
UnhandledException?.Invoke(sender, args);
};
...
AppDomain.CurrentDomain.FirstChanceException += (_, args) =>
{
_lastFirstChanceException = args.Exception;
};
Microsoft.UI.Xaml.Application.Current.UnhandledException += (sender, args) =>
{
var exception = args.Exception;
if (exception.StackTrace is null)
{
exception = _lastFirstChanceException;
}
UnhandledException?.Invoke(sender, new UnhandledExceptionEventArgs(exception, true));
};
While this worked for my use case, SonarLint did indicate that there were some issues with the static constructor approach. One solution to that would be to use a sealed MauiException
class with a Lazy<MauiException>
to access it as a threadsafe singleton, as Jon Skeet describes.
Upvotes: 7
Reputation: 16479
For an app targeting iOS and Android this should be more than enough:
public static class GlobalExceptionHandler
{
// We'll route all unhandled exceptions through this one event.
public static event UnhandledExceptionEventHandler UnhandledException;
static GlobalExceptionHandler()
{
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
UnhandledException?.Invoke(sender, args);
};
// Events fired by the TaskScheduler. That is calls like Task.Run(...)
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
UnhandledException?.Invoke(sender, new UnhandledExceptionEventArgs(args.Exception, false));
};
#if IOS
// For iOS and Mac Catalyst
// Exceptions will flow through AppDomain.CurrentDomain.UnhandledException,
// but we need to set UnwindNativeCode to get it to work correctly.
//
// See: https://github.com/xamarin/xamarin-macios/issues/15252
ObjCRuntime.Runtime.MarshalManagedException += (_, args) =>
{
args.ExceptionMode = ObjCRuntime.MarshalManagedExceptionMode.UnwindNativeCode;
};
#elif ANDROID
// For Android:
// All exceptions will flow through Android.Runtime.AndroidEnvironment.UnhandledExceptionRaiser,
// and NOT through AppDomain.CurrentDomain.UnhandledException
Android.Runtime.AndroidEnvironment.UnhandledExceptionRaiser += (sender, args) =>
{
args.Handled = true;
UnhandledException?.Invoke(sender, new UnhandledExceptionEventArgs(args.Exception, true));
};
Java.Lang.Thread.DefaultUncaughtExceptionHandler = new CustomUncaughtExceptionHandler(e =>
UnhandledException?.Invoke(null, new UnhandledExceptionEventArgs(e, true)));
#endif
}
}
#if ANDROID
public class CustomUncaughtExceptionHandler(Action<Java.Lang.Throwable> callback)
: Java.Lang.Object, Java.Lang.Thread.IUncaughtExceptionHandler
{
public void UncaughtException(Java.Lang.Thread t, Java.Lang.Throwable e)
{
callback(e);
}
}
#endif
And a simple way to use this would be:
GlobalExceptionHandler.UnhandledException += GlobalExceptionHandler_UnhandledException;
private void GlobalExceptionHandler_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as System.Exception;
// Log this exception or whatever
}
Upvotes: 2
Reputation: 585
I solved my problem in Maui .NET 8 on Windows using a recommendation provided by SmartmanApps at https://github.com/dotnet/maui/discussions/653. I haven't tested on other platforms yet, so it might need some adjusting for Android and or iOS/MacCatalyst.
On App.xaml.cs:
public partial class App : Application
{
public App()
{
InitializeComponent();
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
MainPage = new AppShell();
Debug.Print("throwing unhandled exceptions");
throw new Exception("1");
}
private void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
{
Debug.WriteLine($"***** Handling Unhandled Exception *****: {e.Exception.Message}");
// YourLogger.LogError($"***** Handling Unhandled Exception *****: {e.Exception.Message}");
}
}
Note: The application might terminate as before, but we have a chance to observe/log it, and eventually we can deal with any clean-up we'd need to do. Maybe there's a way to prevent it from terminating in some circumnstances?
Upvotes: 4
Reputation: 2951
Struggled with this for a while myself and ended up using Sentry.io, highly impressed so far!
Upvotes: 3