【テクニカル・上級編】Windows Formsにおける堅牢なグローバル例外ハンドリング:AppDomain.UnhandledExceptionで予期せぬクラッシュをキャッチしログを残す – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

泥沼の「突然死」を撲滅せよ:AppDomain.UnhandledExceptionによる究極の防衛線

システム開発の現場において、最も忌むべきは「なぜ落ちたのか分からない」という報告だ。ユーザーの端末でアプリケーションが突如として沈黙する。ログも残らず、イベントビューアには無機質なエラーコードが転がるだけ。これではエンジニアとしての誇りが許さない。

Windows Formsという、イベント駆動の塊のようなアーキテクチャにおいて、グローバルな例外ハンドリングは「実装すべき機能」ではなく「生存のための絶対要件」である。今回は、泥臭いレガシーシステムの最前線でも通用する、堅牢な例外トラップの設計術を伝授する。

1. なぜtry-catchだけでは足りないのか

若手エンジニアはすべてのメソッドをtry-catchで囲おうとするが、それはメモリと可読性を食いつぶす悪手だ。UIスレッドでの例外や、非同期処理の深淵で発生した例外は、ローカルなtry-catchをすり抜ける。

我々が求めるのは、アプリケーションの「死に際」を看取るセーフガードだ。`AppDomain.UnhandledException`は、CLRがアプリケーションを終了させる直前に呼び出される最後の砦である。

2. 堅牢なグローバルハンドラの設計

単に例外をキャッチしてメッセージボックスを出すのは素人の仕事だ。ここでは、スタックトレースを確実にファイルへ書き出し、メモリを解放し、OSへ終了を告げる手順を実装する。

Imports System
Imports System.IO
Imports System.Windows.Forms
Imports System.Threading

”’

”’ アプリケーションの最終防衛ラインを司るクラス
”’

Public NotInheritable Class GlobalExceptionHandler

‘ ログ出力パス(運用時は環境変数等から動的に取得すること)
Private Shared ReadOnly LogFilePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “critical_error.log”)

Public Shared Sub Register()
‘ UIスレッドの例外
AddHandler Application.ThreadException, AddressOf UIThreadExceptionHandler
‘ 非UIスレッドの例外
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf CurrentDomainUnhandledExceptionHandler
End Sub

Private Shared Sub UIThreadExceptionHandler(sender As Object, e As ThreadExceptionEventArgs)
WriteLog(e.Exception, “UIThreadException”)
ShowErrorMessage()
End Sub

Private Shared Sub CurrentDomainUnhandledExceptionHandler(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = DirectCast(e.ExceptionObject, Exception)
WriteLog(ex, “UnhandledException”)
‘ 致命的なエラーなのでOSに制御を戻す前に終了
Environment.Exit(1)
End Sub

Private Shared Sub WriteLog(ex As Exception, type As String)
Try
‘ ストリームを確保し、即座にフラッシュする
Using sw As New StreamWriter(LogFilePath, True, System.Text.Encoding.UTF8)
sw.WriteLine($”[{DateTime.Now:yyyy/MM/dd HH:mm:ss}] TYPE:{type}”)
sw.WriteLine($”MESSAGE: {ex.Message}”)
sw.WriteLine($”STACKTRACE: {ex.StackTrace}”)
sw.WriteLine(“————————————————–“)
End Using
Catch
‘ ログ書き込み自体が失敗した場合は、最悪イベントログを叩く等の代替案が必要
End Try
End Sub

Private Shared Sub ShowErrorMessage()
MessageBox.Show(“予期せぬエラーが発生しました。詳細は管理者へ報告してください。”, “システムエラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Environment.Exit(1)
End Sub
End Class

3. シニアエンジニアが意識すべき「死に際」の美学

このコードを実装する上で、以下の3点は心に刻んでおいてほしい。

① リソース解放の徹底

`UnhandledException`が発火した際、すでにアプリケーションの状態は破壊されている可能性が高い。`Dispose()`を呼ぼうにも、そのオブジェクト自体が壊れている場合がある。ログ出力には、可能な限り依存関係の少ないI/O(ストリームの直書きなど)を使用すること。

② Windows APIとの共生

レガシーなシステム連携を行う際、P/InvokeでWindows APIを呼び出しているケースがあるだろう。API経由のメモリアクセス違反(Access Violation)は、マネージドコードの例外とは次元が異なる。これらを確実に補足するには、`[HandleProcessCorruptedStateExceptions]`属性をメソッドに付与する必要があるが、これは「最後の手段」だ。多用は避けよ。

③ ユーザー体験への配慮

突然の強制終了はユーザーに恐怖を与える。ハンドリング後、可能であれば最小限のログを出し、プロセスをクリーンに終了させる。不完全なメモリ状態でアプリを維持し続けるのが、最も危険な選択であると理解せよ。

結論:システムは「壊れること」を前提に作れ

完璧なコードなど存在しない。我々が構築すべきは、完璧なコードではなく、「壊れたときに正しく死ねるコード」である。

`AppDomain.UnhandledException`は、単なるエラーハンドリングの道具ではない。システムという巨大な機構が、ユーザーの業務を破壊せずに安全に着地するための「航空機におけるブラックボックス」なのだ。

今日、君のプロジェクトにこのハンドラを組み込め。それが、システムの保守コストを劇的に下げ、君を「泥沼のトラブルシューティング」から解放する第一歩となる。

タイトルとURLをコピーしました