Table of Contents

Implement global exception handling

Every application should have global exception handling. Not because you expect things to go wrong, but because things always go wrong in unexpected ways. Without it, you have no record of what happened, and you have no control over how the application responds.

There are two categories of exceptions to handle: expected failures and unexpected exceptions. Expected failures are things you anticipate — invalid input, a failed HTTP call, a missing file. These are best handled close to where they occur. Unexpected exceptions are the ones you didn’t foresee — bugs, null references, third-party library failures. These need a global safety net.


Global exception handling belongs at the entry point of your application. The exact hooks depend on your presentation layer, but the intent is always the same: log what happened, then decide whether the app can continue.

Log unexpected exceptions as Fatal when the application cannot recover, and as Error when it can. An unknown application state is dangerous — it is often better to crash clean and restart than to continue in a potentially corrupt state.

Instead of having no global handler and losing the exception entirely add the appropriate handlers for your platform.


ASP.NET Core API

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var feature = context.Features.Get<IExceptionHandlerFeature>();
        Log.Fatal(feature?.Error, "Unhandled exception");
        context.Response.StatusCode = 500;
    });
});

Avalonia

// Program.cs
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
    Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception");

TaskScheduler.UnobservedTaskException += (_, e) =>
{
    Log.Error(e.Exception, "Unobserved task exception");
    e.SetObserved();
};

// App.axaml.cs
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
    desktop.UnhandledException += (_, e) =>
    {
        Log.Fatal(e.Exception, "UI thread exception");
        e.Handled = true;
    };

UnhandledException fires when the app is already going down — log as Fatal. UnobservedTaskException fires when a faulted Task is garbage collected without being observed — the app survives, so log as Error and call SetObserved() to prevent the crash.


MAUI

MauiExceptions.UnhandledException += (_, e) =>
    Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception");

AppDomain.CurrentDomain.UnhandledException += (_, e) =>
    Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception");

TaskScheduler.UnobservedTaskException += (_, e) =>
{
    Log.Error(e.Exception, "Unobserved task exception");
    e.SetObserved();
};

WPF

// App.xaml.cs
Application.Current.DispatcherUnhandledException += (_, e) =>
{
    Log.Fatal(e.Exception, "UI thread exception");
    e.Handled = true;
};

AppDomain.CurrentDomain.UnhandledException += (_, e) =>
    Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception");

TaskScheduler.UnobservedTaskException += (_, e) =>
{
    Log.Error(e.Exception, "Unobserved task exception");
    e.SetObserved();
};

CLI

AppDomain.CurrentDomain.UnhandledException += (_, e) =>
    Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception");

TaskScheduler.UnobservedTaskException += (_, e) =>
{
    Log.Error(e.Exception, "Unobserved task exception");
    e.SetObserved();
};

AppDomain.CurrentDomain.UnhandledException and TaskScheduler.UnobservedTaskException apply to all platforms and should always be present. The platform-specific hook handles UI thread exceptions for applications that have one.

Global exception handling does not replace handling expected failures close to where they occur. It is the safety net for everything else.

References

© 2026 Rob van der Velden. All rights reserved.