墜落しないアプリケーション:VB.NETにおける「ラスト・ディフェンス」の構築術
業務自動化ツールを開発していると、必ず直面する壁がある。「なぜか現場で突然落ちる」という報告だ。
リリースしたツールが、未知の例外で無言の死(強制終了)を迎える。これほどサポートコストを食い潰し、現場の信頼を失うものはない。原因不明のクラッシュは、開発者にとっての敗北だ。
今日は、VB.NETのWindows Formsアプリケーションにおいて、「アプリケーションの最後の一線」をいかに守るか、その設計思想と実装コードを伝授する。
—
1. なぜ「try-catch」だけでは不十分なのか
初心者は全てのメソッドに `Try…Catch` を貼り付けようとする。だが、それは保守性を破壊する悪手だ。ログの重複、握りつぶし、そして非同期処理やUIスレッド外で発生した例外は、個別のtry-catchでは捕捉できない。
アプリケーションには「予期せぬ事態」が必ず発生する。我々が構築すべきは、個別の処理単位ではなく、プロセス全体を監視するグローバルなセーフティネットだ。
2. 堅牢なグローバル例外ハンドリングの設計
Windows Formsアプリには、例外の伝播を監視するための「入り口」が主に3つ存在する。これらを網羅的に制圧することで、アプリの突然死を防ぐ。
1. `UnhandledException`: 未処理の例外を捕捉する最終防衛ライン。
2. `ThreadException`: UIスレッド上の例外(主にボタンクリック等のイベント内)。
3. `UnobservedTaskException`: 非同期処理(Task)で発生した例外。
これらを `Program.vb`(Mainメソッド)で一括管理するのが、プロの流儀だ。
—
3. 実装コード:プロダクション・レディな設計
以下のコードを `Program.vb`(プロジェクトのスタートアップオブジェクト)に配置してほしい。
Imports System.IO
Imports System.Threading
Module Program
Sub Main()
‘ 1. UIスレッドの例外を捕捉
AddHandler Application.ThreadException, AddressOf Application_ThreadException
‘ 2. UIスレッド以外(非同期処理等)の未処理例外を捕捉
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf CurrentDomain_UnhandledException
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Application.Run(New MainForm())
End Sub
‘ UIスレッドの例外ハンドラ
Private Sub Application_ThreadException(sender As Object, e As ThreadExceptionEventArgs)
LogException(e.Exception, “ThreadException”)
End Sub
‘ プロセス全体の未処理例外ハンドラ
Private Sub CurrentDomain_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
LogException(ex, “UnhandledException”)
End Sub
‘ ログ出力の実装:ここが生命線
Private Sub LogException(ex As Exception, source As String)
Try
Dim logPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “error.log”)
Dim message As String = $”[{DateTime.Now}] Source: {source}{vbCrLf}” &
$”Message: {If(ex IsNot Nothing, ex.Message, “Unknown”)}{vbCrLf}” &
$”StackTrace: {If(ex IsNot Nothing, ex.StackTrace, “N/A”)}{vbCrLf}” &
“——————————————” & vbCrLf
‘ ファイル出力(排他制御が必要ならここでロックをかける)
File.AppendAllText(logPath, message)
MessageBox.Show(“予期せぬエラーが発生しました。ログを出力して終了します。” & vbCrLf &
“詳細は error.log を確認してください。”, “致命的エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Catch
‘ ログ書き込み自体が失敗する場合のリカバリ処理
Finally
‘ プロセスを確実に終了させる
Environment.Exit(1)
End Try
End Sub
End Module
—
4. プロの現場で注意すべき3つの鉄則
このコードをただコピペするだけでは不十分だ。以下の運用上の「重み」を理解せよ。
① ログ出力の「重さ」を知る
ファイルI/Oは低速だ。もしループ処理の中で例外が連続発生したら、ログ書き込みがボトルネックになり、アプリがフリーズする。ログ出力は最小限かつ非同期的に行うか、致命的なエラーのみに絞るべきだ。
② 「環境」への依存を排除する
ログ出力先は、必ず `AppDomain.CurrentDomain.BaseDirectory`(実行ファイルがある場所)を使用すること。`C:\` 直下や保護された領域を指定すると、権限不足で書き込み失敗=アプリ終了の連鎖が発生する。
③ ユーザー体験(UX)を損なわない
「エラーが発生しました」というメッセージを表示する際、スタックトレースをユーザーに見せてはならない。それは攻撃者にヒントを与えるセキュリティリスクであり、ユーザーを混乱させるだけだ。技術的な詳細はログファイルに隔離し、ユーザーには「再起動してサポートに連絡してください」とだけ伝えるのが正しい作法だ。
—
最後に:なぜ「終了」させるのか
`UnhandledException` をキャッチした後、アプリを継続させるのは極めて危険だ。例外が発生した時点で、メモリ上のデータ整合性は保証されていない。
「何事もなかったかのように動く」ことよりも、「確実にログを残し、安全に終了させる」ことの方が、システムの信頼性は遥かに高まる。
この「ラスト・ディフェンス」を実装することは、君が書いたコードの品質を、アマチュアからプロフェッショナルへと昇華させる重要なステップだ。さあ、自分のツールに「保険」をかけよう。
