C# – Global exception event handlers

There are two global exception events available in all .NET applications:

  • FirstChanceException: When any exception is thrown, this event is fired before anything else.
  • UnhandledException: When there’s an unhandled exception, this event is fired right before the process is terminated.

You wire up these event handlers in Main() (before anything else has executed), like this:

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}");
}
Code language: C# (cs)

Note: If you’re using top-level statements, put these statements at the top of the ‘entry point’ file.

This outputs the following before crashing:

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 17Code language: plaintext (plaintext)

Notice the FirstChanceException event fired first. This event is fired before everything else, even catch blocks (I’ll show an example of this below). You can use this for centralized exception logging, instead of needing try/catch blocks just for logging exceptions scattered throughout the code.

In this article, I’ll go into more details about these global exception event handlers, and then I’ll show how they’re used differently in WinForms and ASP.NET Core apps.

The FirstChanceException event with handled exceptions

When an exception happens, it’s routed to the FirstChanceException event first. Then it’s routed to the appropriate catch block. Here’s an example:

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}");
}
Code language: C# (cs)

This outputs the following:

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 19Code language: plaintext (plaintext)

This shows that the FirstChanceException event always fires first.

Corrupted State Exceptions

Corrupted state exceptions (such as access violations in unmanaged code) crash the program and the global exception event handlers aren’t fired. The behavior is different between .NET Core and .NET Framework. I’ll show examples of both below.

First, here’s code that throws an access violation exception:

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);
}
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}");
}
Code language: C# (cs)

.NET Core

Running this in a .NET Core app results in the following exception (written by the framework):

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

It doesn’t route the exception to the exception event handlers.

.NET Framework

The default behavior in a .NET Framework app is similar to the .NET Core behavior. It crashes with the following exception:

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at System.Runtime.InteropServices.Marshal.StructureToPtr(Object structure, IntPtr ptr, Boolean fDeleteOld)Code language: plaintext (plaintext)

It didn’t route the exception to the exception event handlers. However, this behavior can be changed by adding the HandleProcessCorruptedStateExceptions attribute to the methods:

[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}");
}
Code language: C# (cs)

Now it routes the exception to the event handlers before crashing. It outputs the following:

FirstChanceExceptionHandler - Exception=System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at System.Runtime.InteropServices.Marshal.StructureToPtr(Object structure, IntPtr ptr, Boolean fDeleteOld)
   at System.Runtime.InteropServices.Marshal.StructureToPtr[T](T structure, IntPtr ptr, Boolean fDeleteOld)

UnhandledExceptionHandler - Exception=System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at System.Runtime.InteropServices.Marshal.StructureToPtr(Object structure, IntPtr ptr, Boolean fDeleteOld)
   at System.Runtime.InteropServices.Marshal.StructureToPtr[T](T structure, IntPtr ptr, Boolean fDeleteOld)
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 15Code language: plaintext (plaintext)

Notes:

  • This functionality was removed in .NET Core. Even if you use the HandleProcessCorruptedStateExceptions attribute, it’ll be ignored.
  • You can use the legacyCorruptedStateExceptionsPolicy app.config attribute if you don’t want to modify the code.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
    </startup>
	<runtime>
		<legacyCorruptedStateExceptionsPolicy enabled="true" />
	</runtime>
</configuration>
Code language: HTML, XML (xml)

WinForms

WinForms have a third global exception event. It’s called ThreadException. This can be wired up in Main(), just like FirstChanceException and UnhandledException:

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}");
}
Code language: C# (cs)

The ThreadException event fires when an unhandled exception happens in a WinForms thread (such as in a click event handler). If an unhandled exception happens anywhere else, it fires the UnhandledException event instead. I’ll show examples below.

Unhandled exception in a WinForms thread

Control event handlers (like button clicks) are handled in WinForms threads. So here’s an example of an unhandled exception in a WinForms thread:

private void btnThrow_Click(object sender, EventArgs e)
{
	throw new Exception("btnThrow_Click exception");
}
Code language: C# (cs)

Here’s what happens. First, the FirstChanceException event fires:

FirstChanceExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...Code language: plaintext (plaintext)

Then the ThreadException event fires:

ThreadExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...Code language: plaintext (plaintext)

When you don’t use the ThreadException event and an unhandled exception happens in a WinForms thread, the default behavior is that it shows the standard error dialog window stating “Unhandled exception has occurred…”, which is sometimes undesirable. That’s why it’s a good idea to use the ThreadException event.

Unhandled exception anywhere else

The ThreadException event only fires if the exception happened in a WinForms thread. If an unhandled exception happens anywhere else, it fires the UnhandledException event.

Here are two examples of unhandled exceptions in non-WinForms threads:

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();
}
Code language: C# (cs)

In both of these examples, the FirstChanceException event is fired first, followed by the UnhandledException event. Then the app crashes.

The UnhandledException event can be really useful for troubleshooting fatal exceptions in WinForms. Without this, when a fatal unhandled exception happens, the app crashes without any indication of a problem. If an unhandled exception happens before the form is painted, that can be even more difficult to troubleshoot, because you don’t see anything at all.

ASP.NET Core

I wouldn’t suggest using the FirstChanceException event in an ASP.NET Core app. When controllers throw exceptions, this event gets fired repeatedly.

You can use the UnhandledException event to log startup exceptions, like this:

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();
}
Code language: C# (cs)

Let’s say there’s an unhandled exception in Startup.ConfigureServices():

public class Startup
{
	//rest of class
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();

		throw new Exception("Exception in Startup.ConfigureServices");
	}
}
Code language: C# (cs)

When this app starts up, the unhandled exception will cause the UnhandledException event to fire, which logs the following:

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 31Code language: plaintext (plaintext)

Leave a Comment