Steve Peschka
Steve Peschka

Reputation: 1371

How Do You Create A Global Error Handling for a MAUI App

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

Answers (5)

Stephen Quan
Stephen Quan

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

dperish
dperish

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

FreakyAli
FreakyAli

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

Albert
Albert

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

Jimmy
Jimmy

Reputation: 2951

Struggled with this for a while myself and ended up using Sentry.io, highly impressed so far!

Upvotes: 3

Related Questions