Log unhandled exceptions in C#

Use last-chance exception hooks alongside awaited task handling and ASP.NET Core exception middleware, without assuming crash-time logging is guaranteed.

Best practices
Log unhandled exceptions in C#

Register AppDomain.CurrentDomain.UnhandledException near the application entry point to make a last attempt to record an unhandled exception. It is a notification, not a recovery mechanism, and a crashing process cannot guarantee that buffered logs reach their destination.

Register process and task notifications

This example assumes NLog is installed and a target has been configured:

using System;
using System.Threading.Tasks;
using NLog;

class Program
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
        {
            var ex = e.ExceptionObject as Exception;
            Logger.Fatal(ex, "Unhandled exception; terminating={0}", e.IsTerminating);
            LogManager.Flush(TimeSpan.FromSeconds(2));
        };

        TaskScheduler.UnobservedTaskException += (sender, e) =>
        {
            Logger.Error(e.Exception, "An unobserved task exception was reported");
            e.SetObserved();
        };

        // Run the application here.
    }
}

The task event can occur much later, during garbage collection, and might never occur before process exit. Await tasks and handle failures at an appropriate application boundary. SetObserved() marks the exception observed; modern .NET does not normally terminate the process for an unobserved task exception even without that call. Older runtimes and explicitly changed escalation policies need separate consideration.

Keep a last-chance handler short. A bounded flush is only a best effort, especially if the failure involves exhausted resources or a damaged logging dependency.

Handle ASP.NET Core request failures

For request-pipeline exceptions, use exception-handling middleware. In an ASP.NET Core application, register this before the endpoints it should protect:

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var feature = context.Features.Get<IExceptionHandlerFeature>();
        app.Logger.LogError(feature?.Error, "Unhandled request exception");
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await context.Response.WriteAsync("Internal server error");
    });
});

Here, app is the application's WebApplication. Check the framework's own diagnostics and logging configuration to avoid duplicate exception entries. The middleware cannot re-execute an error response after the response has already started.

Frequently asked questions

Is the AppDomain event still useful on .NET Core and .NET 5+?

Yes, as a last-chance notification. ASP.NET Core request exceptions belong at the middleware boundary; background operations also need explicit supervision.

Will an async exception reach UnhandledException?

An exception in an async Task method is stored in its task and rethrown when awaited. What happens next depends on the caller's handling. async void has different propagation rules and can produce an unhandled exception through its synchronization context. A task notification is not a replacement for await and try/catch.

Can the process keep running after the event?

UnhandledException cannot cancel termination. Inspect IsTerminating, record what you can, and let the application follow its failure policy. Do not confuse this with UnobservedTaskException, whose default behavior on modern .NET is nonfatal.

References: task exception notifications and ASP.NET Core error handling.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting