Global exception event handlers in C#

By FoxLearn 3/12/2025 4:25:22 AM   60
In .NET applications, there are two essential global exception events that can help you manage errors:
  1. FirstChanceException: This event is triggered whenever an exception is thrown, before any further processing happens.
  2. UnhandledException: This event is fired when an exception goes unhandled, just before the application is about to terminate.

You can hook these event handlers into the Main() method, ensuring they run before any other code is executed.

using System.Runtime.ExceptionServices;

static void Main(string[] args)
{
    AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionEventHandler;
    AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;

    throw new Exception("Example of unhandled exception");
}

private static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
    Console.WriteLine($"UnhandledExceptionEventHandler - Exception={e.ExceptionObject}");
}

private static void FirstChanceExceptionEventHandler(object sender, FirstChanceExceptionEventArgs e)
{
    Console.WriteLine($"FirstChanceExceptionEventHandler - Exception={e.Exception}");
}

If you're using top-level statements, make sure these event handlers are defined at the start of the entry-point file.

When running the example above, you’ll see output similar to:

FirstChanceExceptionEventHandler - Exception=System.Exception: Example of unhandled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 17

UnhandledExceptionEventHandler - Exception=System.Exception: Example of unhandled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 17

Notice that FirstChanceException is triggered first. This occurs before anything else like catch blocks and can be used for centralized exception logging, eliminating the need for scattered try/catch blocks for logging purposes.

In this article, we'll dive deeper into these global exception events and explore how they behave differently in WinForms and ASP.NET Core applications.

The FirstChanceException Event with Handled Exceptions

The FirstChanceException event is fired first, even before exceptions are routed to a catch block.

AppDomain.CurrentDomain.FirstChanceException += (s, e) => 
    Console.WriteLine($"FirstChanceExceptionEventHandler - Exception={e.Exception}");

try
{
    throw new Exception("Example of handled exception");
}
catch (Exception ex)
{
    Console.WriteLine($"In catch block. Exception={ex}");
}

Output:

FirstChanceExceptionEventHandler - Exception=System.Exception: Example of handled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 19

In catch block. Exception=System.Exception: Example of handled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 19

This shows that FirstChanceException fires before the catch block.

Handling Corrupted State Exceptions

Corrupted state exceptions, such as access violations, can crash the program directly, and global exception handlers aren’t triggered. This behavior differs between .NET Core and .NET Framework. Let’s examine both:

using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;

static void Main(string[] args)
{
    AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionEventHandler;
    AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;

    Marshal.StructureToPtr(1, new IntPtr(1), true); // Access Violation
}

private static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
    Console.WriteLine($"UnhandledExceptionEventHandler - Exception={e.ExceptionObject}");
}

private static void FirstChanceExceptionEventHandler(object sender, FirstChanceExceptionEventArgs e)
{
    Console.WriteLine($"FirstChanceExceptionEventHandler - Exception={e.Exception}");
}

.NET Core Behavior: Running this code in a .NET Core application results in an error that’s not routed to the event handlers:

Fatal error. Internal CLR error. (0x80131506)
   at System.Runtime.InteropServices.Marshal.StructureToPtr(System.Object, IntPtr, Boolean)

.NET Framework Behavior: In .NET Framework, the default behavior is similar, but you can capture corrupted state exceptions by using the [HandleProcessCorruptedStateExceptions] attribute:

[HandleProcessCorruptedStateExceptions]
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    Console.WriteLine($"UnhandledExceptionHandler - Exception={e.ExceptionObject}");
}

[HandleProcessCorruptedStateExceptions]
private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
{
    Console.WriteLine($"FirstChanceExceptionHandler - Exception={e.Exception}");
}

In this case, the exception is routed to the event handlers before the program crashes, producing output like:

FirstChanceExceptionHandler - Exception=System.AccessViolationException: Attempted to read or write protected memory...
UnhandledExceptionHandler - Exception=System.AccessViolationException: Attempted to read or write protected memory...

However, note that .NET Core ignores the [HandleProcessCorruptedStateExceptions] attribute. If you want to handle these exceptions in .NET Framework, you can enable legacy behavior via the legacyCorruptedStateExceptionsPolicy setting in app.config:

<configuration>
    <runtime>
        <legacyCorruptedStateExceptionsPolicy enabled="true" />
    </runtime>
</configuration>

WinForms Exception Handling

WinForms introduces a third global exception event called ThreadException, which captures unhandled exceptions in WinForms threads (e.g., events triggered by button clicks).

using System.Runtime.ExceptionServices;

[STAThread]
static void Main()
{
    Application.ThreadException += ThreadExceptionEventHandler;
    AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionEventHandler;
    AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;

    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new frmMain());
}

private static void ThreadExceptionEventHandler(object sender, System.Threading.ThreadExceptionEventArgs e)
{
    MessageBox.Show($"ThreadExceptionEventHandler - Exception={e.Exception}");
}

private static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
    MessageBox.Show($"UnhandledExceptionEventHandler - Exception={e.ExceptionObject}");
}

private static void FirstChanceExceptionEventHandler(object sender, FirstChanceExceptionEventArgs e)
{
    MessageBox.Show($"FirstChanceExceptionEventHandler - Exception={e.Exception}");
}

In WinForms, the ThreadException event will trigger when an unhandled exception occurs on a WinForms thread, such as within a control event handler. However, if the unhandled exception occurs elsewhere, it will trigger the UnhandledException event.

For example: Unhandled Exception in a WinForms Thread

private void btnThrow_Click(object sender, EventArgs e)
{
    throw new Exception("btnThrow_Click exception");
}

Output:

FirstChanceExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...
ThreadExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...

Without handling the ThreadException event, WinForms would display the standard error dialog, which may not be desirable in production applications. Handling this event allows you to prevent the app from crashing unexpectedly.

Unhandled Exception in Non-WinForms Thread

public frmMain()
{
    InitializeComponent();
    throw new Exception("Exception in form constructor");
}

private void btnThrow_Click(object sender, EventArgs e)
{
    var thread = new System.Threading.Thread(() =>
    {
        throw new Exception("Exception in a non-WinForms thread");
    });
    thread.Start();
}

For both cases, FirstChanceException is fired first, followed by UnhandledException, and then the app crashes.

Using FirstChanceException in ASP.NET Core

For ASP.NET Core, it’s generally not recommended to use the FirstChanceException event. When exceptions are thrown within controllers, this event is fired repeatedly, making it less useful.

Instead, you can use the UnhandledException event to log startup exceptions:

using NLog;

private static Logger logger = LogManager.GetCurrentClassLogger();

public static void Main(string[] args)
{
    AppDomain.CurrentDomain.UnhandledException += (s, e) =>
    {
        logger.Error($"UnhandledExceptionHandler - Exception={e.ExceptionObject}");
        LogManager.Flush();
    };

    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        }).Build().Run();
}

If an exception occurs during startup, such as in ConfigureServices, the UnhandledException event will log the error:

2021-09-09 15:57:51.6949 ERROR UnhandledExceptionHandler - Exception=System.Exception: Exception in Startup.ConfigureServices
   at ExampleWebApp.Startup.ConfigureServices(IServiceCollection services) in Startup.cs:line 31

These tools can help you improve error handling, debug issues, and make your application more resilient to unexpected failures.