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
- AppDomain.UnhandledException - Microsoft Learn
- TaskScheduler.UnobservedTaskException - Microsoft Learn
- Error handling in ASP.NET Core - Microsoft Learn