【テクニカル・上級編】Windows Formsにおけるグローバル例外ハンドリングの極意:ThreadExceptionとUnhandledExceptionの使い分け – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsにおけるグローバル例外ハンドリングの極意:ThreadExceptionとUnhandledExceptionの使い分け

レガシーシステムの保守からモダンな業務アプリケーションの構築まで、Windows Forms(WinForms)開発において避けて通れない課題が「予期せぬ例外の捕捉」である。

現場のエンジニアであれば、ユーザーが業務の佳境で突如として「アプリケーションは動作を停止しました」という無機質なダイアログに直面し、それまでの入力データが露と消える絶望的な瞬間を何度も目撃していることだろう。

プログラミングとは例外処理の歴史に他ならない。しかし、個別の `Try-Catch` ブロックをどれほど綿密に配置しようとも、想定外の非同期処理やUIスレッドの深部で発生する致命的な例外は、いとも容易く防壁をすり抜ける。

今回は、VB.NETによるWindows Formsアプリケーションにおいて、アプリのクラッシュを完全に制御下置き、安全なログ出力と終了処理を実現するための「グローバル例外ハンドリング」の極意を、ランタイムの挙動とメモリ管理の観点から徹底的に解剖する。

—

1. 2つの例外フック:`ThreadException` と `UnhandledException` の本質的な違い

WinFormsでグローバル例外を捕捉するためには、主に2つの異なるレイヤーのイベントを監視する必要がある。

  • `Application.ThreadException` (UIスレッド起因)
  • `AppDomain.CurrentDomain.UnhandledException` (非UIスレッドおよびAppDomain全体)

この2つを混同し、片方だけを実装しているシステムは、実運用において必ずクラッシュを引き起こす。それぞれのライフサイクルと適用領域の境界線を正確に理解せよ。

表:例外フックの比較

| 特性 | `Application.ThreadException` | `AppDomain.CurrentDomain.UnhandledException` |
| :— | :— | :— |
| 発生スレッド | UIスレッド(メインメッセージループ) | 非UIスレッド、ThreadPool、または致命的例外 |
| アプリの継続可能性 | 可能(イベント内で処理を継続・終了を選択できる) | 不可能(原則としてプロセスは強制終了する) |
| 主な用途 | ユーザー操作起因のエラー、UIイベント内の例外 | バックグラウンド処理、タイマー、タスク非同期例外 |

—

2. 堅牢なエラーフック機構のアーキテクチャ設計

実際のエンタープライズ環境では、例外を捕捉したあとに以下の処理をアトミックに実行する必要がある。
1. 多重起動(再入)の防止: エラー処理中にさらなる例外が発生した際の無限ループを防ぐ。
2. 詳細な診断情報の収集: スタックトレースだけでなく、OSバージョン、CLRバージョン、メモリ使用量を記録する。
3. 安全な永続化: ネットワーク切断を考慮し、まずはローカルファイルシステムへ確実にダンプを出力する。
4. ユーザーへの優雅な通知: 処理を中断し、情け容赦なくプロセスを安全にシャットダウンする。

以下のコードは、これらを極限まで考慮したVB.NETによる実装例である。

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

Namespace Enterprise.ExceptionManagement

Public NotInheritable Class GlobalExceptionHandler

‘ 多重トラップを防ぐためのフラグ(原子性を確保)
Private Shared _isHandling As Integer = 0

”’

”’ グローバル例外ハンドラの初期化(Program.vbのMainメソッドから呼気すること)
”’

Public Shared Sub Register()
‘ 1. UIスレッドの例外をフック
AddHandler Application.ThreadException, AddressOf UIThreadException_Handler

‘ 2. 非UIスレッド・AppDomain全体の例外をフック
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf AppDomainUnhandledException_Handler

‘ 3. JITコンパイラ最適化によるハンドラ脱落を防ぐ設定等(必要に応じ)
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchExceptionData)
End Sub

”’

”’ UIスレッドで発生した例外のハンドラ
”’

Private Shared Sub UIThreadException_Handler(sender As Object, e As ThreadExceptionEventArgs)
Try
‘ Interlockedによるスレッドセーフな排他制御
If Interlocked.Exchange(_isHandling, 1) = 0 Then
ProcessFatalException(e.Exception, “UI_Thread_Exception”)
End If
Catch ex As Exception
‘ ハンドラ内部での例外はOSのフェイルファストに委ねる
Environment.FailFast(“Critical error in ThreadException handler.”, ex)
End Try
End Sub

”’

”’ 非UIスレッドおよびAppDomain全体の未処理例外ハンドラ
”’

Private Shared Sub AppDomainUnhandledException_Handler(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
If ex Is Nothing Then
ex = New Exception(String.Format(“Non-Exception object thrown: {0}”, e.ExceptionObject))
End If

Try
If Interlocked.Exchange(_isHandling, 1) = 0 Then
ProcessFatalException(ex, “AppDomain_Unhandled_Exception”)
End If
Catch innerEx As Exception
Environment.FailFast(“Critical error in UnhandledException handler.”, innerEx)
Finally
‘ UnhandledExceptionの場合、プロセスは強制終了に向かうため、必要ならここでリソースの強制解放を行う
End Try
End Sub

”’

”’ 例外情報のダンプ出力とユーザー通知の中核処理
”’

Private Shared Sub ProcessFatalException(ex As Exception, errorContext As String)
‘ 1. 診断情報の構築
Dim logBuilder As New System.Text.StringBuilder()
logBuilder.AppendLine(“==================================================”)
logBuilder.AppendLine(String.Format(“Timestamp: {0:yyyy-MM-dd HH:mm:ss.fff}”, DateTime.Now))
logBuilder.AppendLine(String.Format(“Context: {0}”, errorContext))
logBuilder.AppendLine(String.Format(“OS Version: {0}”, Environment.OSVersion))
logBuilder.AppendLine(String.Format(“.NET Runtime: {0}”, Environment.Version))
logBuilder.AppendLine(String.Format(“Working Set: {0:N0} KB”, Environment.WorkingSet / 1024))
logBuilder.AppendLine(“————————————————–“)
logBuilder.AppendLine(“Exception Details:”)
logBuilder.AppendLine(ex.ToString())
logBuilder.AppendLine(“==================================================”)

‘ 2. ローカルストレージへの緊急書き込み(ファイルI/Oの失敗に備える)
Dim logFilePath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “FatalError.log”)
Try
File.AppendAllText(logFilePath, logBuilder.ToString(), System.Text.Encoding.UTF8)
Catch
‘ ディスク容量不足やアクセス権限がない場合のフォールバック(イベントログへ書き込み)
EventLog.WriteEntry(“Application”, logBuilder.ToString(), EventLogEntryType.Error)
End Try

‘ 3. ユーザーへの優雅な通知とシャットダイアログ
MessageBox.Show(
“システムで回復不可能なエラーが発生しました。” & Environment.NewLine &
“アプリケーションを終了します。” & Environment.NewLine &
“詳細はログファイルを確認してください。” & Environment.NewLine &
“ログ出力先: ” & logFilePath,
“致命的なエラー”,
MessageBoxButtons.OK,
MessageBoxIcon.Error
)

‘ 4. プロセス全体の安全な終了
Environment.Exit(1)
End Sub

End Class

End Namespace

—

3. シニアエンジニアが知るべき実装上の罠とメモリ最適化

ここまでのコードは基本形に過ぎない。現場で数百万ユーザーを抱える、あるいは24時間稼働するレガシー連携システムに組み込む場合、以下の「見えないコストとリスク」を制御しなければならない。

1. 例外オブジェクト生成のメモリフットプリント

`New Exception()` やスタックトレースの生成は、CLRにとって非常に重い処理である。特にスタックトレースの構築は、マネージドヒープに多数の文字列オブジェクトを一時アロケーションする。
メモリがひっ迫している極限状態(OutOfMemoryExceptionの直前など)において、エラーハンドラ内でさらに重いオブジェクトを生成すると、ハンドラ自体が例外をスローしてプロセスが異常終了(FailFast)する。
そのため、致命的な例外処理においては、マネージドヒープへの無駄なアロケーションを極力避け、必要最低限のバッファサイズで文字列を構築することが求められる。

2. COMオブジェクトとWindows APIのクリーンアップ

レガシーなVB.NETアプリケーションでは、Office連携(Excel/Wordオートメーション)や独自COMコンポーネントを使用しているケースが多い。
グローバル例外でアプリが強制終了する場合、背後で起動した外部プロセス(`excel.exe` など)がゾンビプロセスとしてメモリ上に残留し、リソースリークを引き起こす。
`UnhandledException` のフェーズや、アプリケーションの終了イベントにおいて、確実に `Marshal.ReleaseComObject` やガベージコレクションの強制実行(`GC.Collect()` / `GC.WaitForPendingFinalizers()`)を行う設計を組み込むべきである。

‘ ガベージコレクションとCOM解放の強制実行(終了直前)
Friend Sub ForceReleaseResources()
Try
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
Catch
‘ 終了直前の例外は握りつぶす
End Try
End Sub

3. 非同期(Async/Await)処理における例外の罠

VB.NETの `Async Sub` は、例外がスローされても呼び出し元に伝播せず、自動的に `AppDomain.CurrentDomain.UnhandledException` に直行する。これが原因で、`Try-Catch` で囲んだつもりでもアプリがクラッシュする現象が多発する。
業務ロジックやイベントハンドラで非同期処理を使用する場合は、必ず `Async Function` として定義し、戻り値として `Task` を返すことで、呼び出し元で例外を制御下におけるアーキテクチャに統一せよ。

—

総括

グローバル例外ハンドリングとは、単なる「クラッシュ防止の安全装置」ではない。それは、システムが予期せぬ破滅を迎えた際、最期の瞬間まで正確に診断情報を残し、ビジネスデータの整合性を守るための「エンジニアリングの美学」である。

フレームワークに処理を丸投げするのではなく、スレッドモデル、メモリのライフサイクル、そしてOSとの境界線を熟知した上でコードを組み上げる。それこそが、真に信頼されるエンタープライズ・アプリケーションを支える唯一の道である。

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