【実務・中級編】VB.NETアプリケーションのクラッシュレス化:AppDomain.CurrentDomain.UnhandledExceptionによる最終防衛ラインのエラーキャッチ – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETアプリケーションのクラッシュレス化:AppDomain.CurrentDomain.UnhandledExceptionによる最終防衛ラインのエラーキャッチ

開発現場で業務効率化ツールや社内ニッチシステムをVB.NETで構築していると、避けて通れない悪夢がある。それは、テスト環境では一度も再現しなかった「原因不明の致命的な例外(Fatal Exception)」による、予期せぬアプリケーションの強制終了だ。

ユーザーが入力途中のデータを失い、エラーログすら残さずにタスクマネージャーから消え去るアプリケーション――。これほど現場の信頼を失墜させるものはない。「なぜあの時、あの例外をキャッチできなかったのか?」と頭を抱えた経験を持つエンジニアは多いはずだ。

今回は、VB.NETアプリケーションにおけるクラッシュレス化(堅牢化)の極限、すなわち「最後の砦」である `AppDomain.CurrentDomain.UnhandledException` を用いた最終防衛ラインの構築手法を伝授する。

なぜ通常の `Try-Catch` では防げないのか?

初学者が陥りやすい罠が、「すべてのメソッドを `Try-Catch` で囲めばクラッシュしない」という誤解だ。

‘ 【アンチパターン】すべての処理をTry-Catchで囲む愚行
Sub Main()
Try
‘ 業務ロジック
Catch ex As Exception
‘ ログを出して耐え忍ぶつもり…
End Sub
End Sub

このアプローチが破綻する理由は明確である。
1. 非同期処理 (Async/Await) やバックグラウンドスレッドで発生した未処理例外は、呼び出し元スレッドの `Try-Catch` をすり抜ける。
2. 栈オーバーフロー (StackOverflowException)メモリ不足 (OutOfMemoryException) など、CLR(共通言語ランタイム)の実行基盤そのものを揺るがす致命的なエラーは、通常の `Catch Exception` では捕捉できない。

特に、業務自動化ツールでよく使われる「外部プロセス連携」「マルチスレッドによる並列ファイル処理」「COMオブジェクトの操作」において、メインスレッド以外での例外爆発は日常茶飯事だ。

ここで発動するのが、`AppDomain.CurrentDomain.UnhandledException` である。これは、プロセスが死の淵にある瞬間(プロセス終了の直前)に、アプリケーションドメイン全体でキャッチされなかった例外を強制的に拾い上げる、文字通りの「最終防衛ライン」である。

堅牢なフォールバック・アーキテクチャの設計

プロセスの強制終了を防ぐことはできないが(致命的例外の場合、CLRの仕様上プロセスは終了に向かう)、「何も言わずに消える」のを防ぎ、データを守り、優美な死を迎えることは可能だ。

プロダクションコードとして耐えうる堅牢な設計では、以下の3点を担保する必要がある。

1. 二重起動・多重ログ出力の防止
クラッシュ時には複数スレッドから同時にイベントが発火することがあるため、排他制御(MutexやSyncLock)が必須。
2. 最小限の依存関係でのログ・ダンプ出力
すでにCLRが不安定な状態にあるため、複雑なサードパーティ製ライブラリの呼び出しは禁物。標準ライブラリ(`System.IO` 等)で完結させる。
3. ユーザーへの適切なフィードバック
冷たい例外スタックトレースではなく、「作業データを退避しました。システム管理者に以下のファイルを送付してください」という業務継続可能なメッセージの提示。

【コピペ即導入】プロダクション・グレード実装コード

以下のコードは、Windows Forms または Console アプリケーションののエントリーポイント(通常は `Application.ThreadException` と組み合わせる)で実装する、実戦投入仕様のモジュールである。

Imports System.IO
Imports System.Threading

Module Program

‘ 多重クラッシュイベント発火を防ぐためのフラグ
Private _IsCrashing As Boolean = False
Private ReadOnly _LockObj As New Object()

Sub Main()
‘ 1. UIスレッドの未処理例外をキャッチ (Windows Formsの場合)
AddHandler Application.ThreadException, AddressOf OnUIThreadException

‘ 2. 非UIスレッド(バックグラウンド等)の未処理例外をキャッチ
AddHandler Thread.GetDomain().UnhandledException, AddressOf OnUnhandledException

‘ — アプリケーションの通常起動シーケンス —
Console.WriteLine(“アプリケーションを起動します…”)

‘ あえて未処理例外を発生させるテスト (コメントアウトを外して確認)
‘ Dim t As New Thread(Sub() Throw New InvalidOperationException(“バックグラウンドで予期せぬ爆発”))
‘ t.Start()
‘ t.Join()

Application.Run(New MainForm()) ‘ ※コンソールアプリの場合は適宜書き換え
End Sub

”’

”’ UIスレッド側での例外ハンドラ
”’

Private Sub OnUIThreadException(sender As Object, e As ThreadExceptionEventArgs)
HandleFatalException(e.Exception, “UIThreadException”)
End Sub

”’

”’ アプリケーションドメイン全体での最終防衛ライン(非UIスレッド等)
”’

Private Sub OnUnhandledException(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
Dim origin As String = If(e.IsTerminating, “AppDomain_FatalTerminating”, “AppDomain_Unhandled”)

HandleFatalException(ex, origin)
End Sub

”’

”’ 致命的例外を処理する中核ロジック(スレッドセーフ)
”’

Private Sub HandleFatalException(ex As Exception, origin As String)
SyncLock _LockObj
If _IsCrashing Then Return
_IsCrashing = True
End SyncLock

Try
‘ 1. 最小限のコストでローカルディスクに緊急ダンプ(ログ)を出力
WriteEmergencyLog(ex, origin)

‘ 2. ユーザーへの優美な通知
MessageBox.Show(
“システムで予期せぬ致命的なエラーが発生しました。” & vbCrLf &
“作業内容は可能な限り保護されましたが、アプリケーションを終了します。” & vbCrLf &
“詳細は ‘emergency_error.log’ をご確認ください。”,
“致命的なエラー”,
MessageBoxButtons.OK,
MessageBoxIcon.Error
)

Catch writeEx As Exception
‘ ログ書き込みすら失敗した場合の最後の手段
File.WriteAllText(“C:\Temp\fatal_emergency.txt”, “Logger crashed: ” & writeEx.ToString())
Finally
‘ プロセスを安全に強制終了(リソースリークを防ぐ)
Environment.Exit(1)
End Try
End Sub

”’

”’ 外部依存を排除した堅牢なファイル出力
”’

Private Sub WriteEmergencyLog(ex As Exception, origin As String)
Dim logPath As Path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “emergency_error.log”)
Dim sb As New System.Text.StringBuilder()

sb.AppendLine(“========================================”)
sb.AppendLine($”Timestamp: {DateTime.Now:yyyy-MM-dd HH:mm:ss}”)
sb.AppendLine($”Origin: {origin}”)
If ex IsNot Nothing Then
sb.AppendLine($”Exception Type: {ex.GetType().FullName}”)
sb.AppendLine($”Message: {ex.Message}”)
sb.AppendLine($”StackTrace: {ex.StackTrace}”)
If ex.InnerException IsNot Nothing Then
sb.AppendLine($”Inner Message: {ex.InnerException.Message}”)
End If
Else
sb.AppendLine(“ExceptionObject is Nothing or not an Exception type.”)
End If
sb.AppendLine(“========================================” & vbCrLf)

‘ 共有違反を防ぐためにAppendTextを使用
File.AppendAllText(logPath, sb.ToString(), System.Text.Encoding.UTF8)
End Sub

End Module

チーフアーキテクトからの実務アドバイス

このパターンを現場に導入する際、以下の「DB・ファイル連携における罠」に留意してほしい。

1. データベーストランザクションのロールバック過信
`UnhandledException` が走る瞬間、DBコネクションがすでに断絶しているか、あるいは接続プールが異常な状態にある可能性が高い。このイベント内で「無理やりDBにエラーログを書き込もうとする」のは自殺行為だ(二重例外を引き起こし、アプリがフリーズする)。ログは必ずローカルファイルシステムに吐き出すこと。
2. ファイルの排他制御
業務ツールがCSVやExcelファイルを頻繁に読み書きしている場合、クラッシュの瞬間にファイルハンドルが開きっぱなしになっていることがある。`Finally` ブロックやガベージコレクションの過信は禁物であり、OSレベルでのリソース解放は `Environment.Exit(1)` によって強制的に行われることを意識せよ。

まとめ

プログラミングにおいて「バグをゼロにする」ことは幻想に過ぎない。真に優秀なアーキテクトが作るシステムとは、「バグや予期せぬ環境要因で例外が起きても、決して見苦しく散らない、気高く堅牢なシステム」のことである。

`AppDomain.CurrentDomain.UnhandledException` を制し、あなたのVB.NETアプリケーションに真のプロフェッショナルな耐障害性を実装してほしい。

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