C# – グローバル例外イベント ハンドラー

すべての .NET アプリケーションで使用できる 2 つのグローバル例外イベントがあります:

  • FirstChanceException:例外がスローされると、このイベントは何よりも先に発生します。
  • UnhandledException:未処理の例外がある場合、このイベントはプロセスが終了する直前に発生します。

次のように、これらのイベント ハンドラーを Main() で (他の処理が実行される前に) 結び付けます。

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)

これにより、クラッシュする前に次のように出力されます:

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)

最初に発生した FirstChanceException イベントに注意してください。このイベントは、catch であっても、他の何よりも先に発生します ブロック (この例を以下に示します)。 try/catch を必要とする代わりに、これを集中例外ロギングに使用できます。 コード全体に散在する例外をログに記録するためだけのブロック。

この記事では、これらのグローバル例外イベント ハンドラーについて詳しく説明し、WinForms アプリと ASP.NET Core アプリでの使用方法の違いを示します。

例外が処理された FirstChanceException イベント

例外が発生すると、最初に FirstChanceException イベントにルーティングされます。次に、適切な catch ブロックにルーティングされます。

以下に例を示します:

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)

これは以下を出力します:

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)

これは、FirstChanceException イベントが常に最初に発生することを示しています。

破損状態の例外

破損状態の例外 (アンマネージ コードでのアクセス違反など) によってプログラムがクラッシュし、グローバル例外イベント ハンドラーが起動されません。 .NET Core と .NET Framework では動作が異なります。以下に両方の例を示します。

まず、アクセス違反の例外をスローするコードは次のとおりです:

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 コア

これを .NET Core アプリで実行すると、次の例外が発生します (フレームワークによって書き込まれます):

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

例外を例外イベント ハンドラにルーティングしません。

.NET フレームワーク

.NET Framework アプリの既定の動作は、.NET Core の動作に似ています。次の例外でクラッシュします:

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)

例外を例外イベント ハンドラーにルーティングしませんでした。ただし、この動作は、HandleProcessCorruptedStateExceptions 属性をメソッドに追加することで変更できます:

[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)

クラッシュする前に例外をイベント ハンドラーにルーティングするようになりました。以下を出力します:

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)

注:

  • この機能は .NET Core で削除されました。 HandleProcessCorruptedStateExceptions 属性を使用しても無視されます。
  • コードを変更したくない場合は、legacyCorruptedStateExceptionsPolicy app.config 属性を使用できます。
<?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 には、3 番目のグローバル例外イベントがあります。これは ThreadException と呼ばれます。これは、FirstChanceException や UnhandledException と同様に、Main() で関連付けることができます:

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)

ThreadException イベントは、未処理の例外が WinForms スレッド (クリック イベント ハンドラーなど) で発生したときに発生します。未処理の例外が他の場所で発生した場合は、代わりに UnhandledException イベントが発生します。以下に例を示します。

WinForms スレッドで未処理の例外

コントロール イベント ハンドラー (ボタン クリックなど) は、WinForms スレッドで処理されます。以下は、WinForms スレッドで未処理の例外の例です:

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

これが何が起こるかです。まず、FirstChanceException イベントが発生します:

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

その後、ThreadException イベントが発生します:

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

ThreadException イベントを使用せず、WinForms スレッドで未処理の例外が発生した場合、既定の動作では、「未処理の例外が発生しました...」という標準エラー ダイアログ ウィンドウが表示されますが、これは望ましくない場合があります。そのため、ThreadException イベントを使用することをお勧めします。

他の場所で未処理の例外

ThreadException イベントは、例外が WinForms スレッドで発生した場合にのみ発生します。未処理の例外が他の場所で発生した場合、UnhandledException イベントが発生します。

WinForms 以外のスレッドで処理されない例外の 2 つの例を次に示します。

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)

どちらの例でも、FirstChanceException イベントが最初に発生し、その後に UnhandledException イベントが発生します。その後、アプリがクラッシュします。

UnhandledException イベントは、WinForms での致命的な例外のトラブルシューティングに非常に役立ちます。これがないと、処理されない致命的な例外が発生したときに、アプリは何の問題もなくクラッシュします。フォームが描画される前に未処理の例外が発生した場合、何も表示されないため、トラブルシューティングがさらに困難になる可能性があります。

ASP.NET コア

ASP.NET Core アプリで FirstChanceException イベントを使用することはお勧めしません。コントローラーが例外をスローすると、このイベントが繰り返し発生します。

UnhandledException イベントを使用して、次のように起動例外をログに記録できます:

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)

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)

このアプリが起動すると、未処理の例外によって UnhandledException イベントが発生し、次のログが記録されます:

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)