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

スポンサーリンク

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

現場のリーダーとしてハッキリと言わせてもらう。
業務自動化ツールや社内ニッチシステムをWindows Formsで構築する際、最も恥ずべき瞬間は何か?

それは、ユーザーが重要なデータを入力し終え、エンターキーを押した瞬間に、「処理しきれない例外が発生しました」という味気ない.NET Framework標準のダイアログとともに、アプリが音もなく強制終了する瞬間だ。

「なぜ落ちたのか分からない」「入力したデータが消えた」「社内からヘルプデスクに怒りの電話が鳴り響く」。
これを防ぐのが、プロフェッショナルなエンジニアの仕事である。

今回は、Windows Formsアプリケーションにおける「グローバル例外ハンドリング」の極意を授ける。`Application.ThreadException`と`AppDomain.CurrentDomain.UnhandledException`の決定的な違いを理解し、現場で二度と「原因不明のクラッシュ」を起こさないための鉄壁の防衛網を構築しよう。

—

1. なぜ「Try-Catch」だけでは不十分なのか?

初学者や、場当たり的なコーディングをするプログラマブルなだけのコードは、すべてのメソッドの内部に`Try-Catch`をばら撒く。

「非効率極まりない。」

すべてのボタンクリックやイベントハンドラーに同じエラーログ出力のボイラープレートコードを書くのか? それは保守性をドブに捨てる行為だ。さらに言えば、非同期処理(Async/Await)やバックグラウンドスレッド、UIスレッドの深部で発生した予期せぬ例外は、局所的な`Try-Catch`をすり抜けてアプリケーションを容赦なくクラッシュさせる。

ここで登場するのが「グローバル例外ハンドラー」だ。アプリ全体のどこかでキャッチされなかった例外を、最後の砦として一網打尽にする。

しかし、Windows Formsの世界には、捕捉すべき「2つの異なる例外経路」が存在することを理解している者は少ない。

—

2. 2大例外トラップの正体と使い分け

Windows Formsアプリでは、例外の発生源によってフックすべきイベントが異なる。ここを混同すると、例外を拾い損ねてアプリが落ちる。

① `Application.ThreadException` (UIスレッドの守護神)

  • 対象: UIスレッド(フォームやボタンなどのコントロール操作が行われるスレッド)上で発生した未処理例外。
  • 特徴: 例外発生後、アプリケーションを継続(Continue)させるか、終了させるかを選択できる。ユーザーに「エラーが発生しましたが、作業を続行しますか?」と問いかけ、データを保存する猶予を与えることが可能。

② `AppDomain.CurrentDomain.UnhandledException` (最終防衛ライン)

  • 対象: バックグラウンドスレッド、または非UIスレッドで発生し、誰もキャッチしなかった未処理例外。
  • 特徴: ここに到達した時点で、CLR(共通言語ランタイム)は致命的な状態と判断している。アプリの継続は不可能であり、このイベントが発生した直後にアプリケーションは強制終了する。ここでは「泣きながらログを吐き、管理者にメールを飛ばし、最低限のクリーンアップを行う」ことしかできない。

> 【アーキテクトの鉄則】
> 業務アプリでは、この両方を必ず実装しなければならない。UIスレッドの事故とバックグラウンドの事故では、アプローチが根本的に異なるからだ。

—

3. 【コピペOK】プロダクション品質の例外ハンドリング実装

以下のコードは、Program.cs(VB.NETの場合は `ApplicationEvents.vb` または Main Sub)に記述する、実戦投入可能な最高峰のブートストラップ設計だ。

今回はVB.NETのWindows Formsプロジェクトを想定し、メインエントリーポイントでの設定例を示す。

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

Friend NotInheritable Class Program


Friend Shared Sub Main()
‘ 1. Windows Formsの視覚スタイルとビジュアル設定
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)

‘ ==========================================
‘ 2. グローバル例外ハンドラーの登録
‘ ==========================================

‘ UIスレッド上の未処理例外をキャッチ (継続可能)
AddHandler Application.ThreadException, AddressOf UIThreadException_Handler

‘ CLR全体の未処理例外をキャッチ (非UIスレッド用 / 終了直前)
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf NonUIThreadException_Handler

‘ ==========================================
‘ 3. アプリケーション起動
‘ ==========================================
Application.Run(New MainForm())
End Sub

‘ — UIスレッド例外ハンドラー —
Private Shared Sub UIThreadException_Handler(sender As Object, e As ThreadExceptionEventArgs)
Dim ex As Exception = e.Exception

‘ ログ出力と管理者通知の実行
LogException(ex, “UI Thread Exception”)

‘ ユーザーへの優しい通知と継続判断
Dim result As DialogResult = MessageBox.Show(
“予期せぬエラーが発生しました。” & vbCrLf &
“作業を継続しますか?(「いいえ」を選ぶと安全に終了します)” & vbCrLf & vbCrLf &
“詳細: ” & ex.Message,
“システムエラー”,
MessageBoxButtons.YesNo,
MessageBoxIcon.Error)

If result = DialogResult.No Then
Application.Exit()
End If
End Sub

‘ — 非UIスレッド例外ハンドラー (致命的) —
Private Shared Sub NonUIThreadException_Handler(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = CType(e.ExceptionObject, Exception)

‘ 致命的なため、確実なログ出力と管理者への緊急通知を行う
LogException(ex, “Fatal Non-UI Thread Exception (App Crashing)”)

MessageBox.Show(
“アプリケーションで回復不能な致命的エラーが発生しました。” & vbCrLf &
“アプリケーションを終了します。”,
“致命的エラー”,
MessageBoxButtons.OK,
MessageBoxIcon.Stop)
End Sub

‘ — 共通ログ出力・通知ロジック —
Private Shared Sub LogException(ex As Exception, errorType As String)
Try
Dim logDir As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “Logs”)
If Not Directory.Exists(logDir) Then
Directory.CreateDirectory(logDir)
End If

Dim logFile As String = Path.Combine(logDir, “error_” & DateTime.Now.ToString(“yyyyMMdd”) & “.log”)

Dim logContent As String = String.Format(
“[{0}] [{1}]” & vbCrLf & “Message: {2}” & vbCrLf & “StackTrace:” & vbCrLf & “{3}” & vbCrLf & new String(“-“c, 50) & vbCrLf,
DateTime.Now.ToString(“yyyy-MM-dd HH:mm:ss”),
errorType,
ex.Message,
ex.StackTrace)

‘ ファイル書き込み (排他制御を考慮しつつシンプルに実装)
File.AppendAllText(logFile, logContent)

‘ 【発展】ここにSMTPを使った管理者への自動メール送信ロジックを組み込むと完璧。

Catch logEx As Exception
‘ ログ書き込み自体が失敗した場合の最後の手段
Diagnostics.Debug.WriteLine(“ログ書き込み失敗: ” & logEx.Message)
End Try
End Sub

End Class

—

4. ファイル・DB連携アプリにおける「最後の安全弁」としての注意点

業務自動化ツールでは、データベース(SQLite, SQL Server, Access等)への書き込み中や、Excelファイル操作中にクラッシュすることが多い。

グローバル例外ハンドラーは「最後の砦」であって、トランザクション管理の身代わりではない。これらを実装する上での注意点を述べる。

1. トランザクションのロールバック漏れを防ぐ
DB更新処理の最中に例外でアプリが落ちた場合、DB側でロックや中途半端なデータ(汚染データ)が残る。`Try-Finally`や`Using`構文を徹底し、例外発生時には確実にロールバックされる構造を作った上で、例外を上位(グローバル)にスローさせること。
2. ログ出力先権限の罠
社内PCツールでありがちなのが、`C:\Program Files\YourApp\Logs\`などにログを出そうとして、書き込み権限エラー(UnauthorizedAccessException)でログすら残らない悲劇。ログは必ずユーザープロファイル配下(`Environment.SpecialFolder.ApplicationData`)または、実行ファイルと同等のローカル安全領域に出力せよ。
3. 多重起動の防止とデバッグ時の挙動
グローバル例外ハンドラーを有効にすると、Visual Studioでデバッグ中にわざと例外を発生させた際もハンドラーがフックしてしまう。開発時はデバッガーアタッチ状態(`Debugger.IsAttached`)を判定して、ハンドラーをバイパスするか、そのままスローさせると開発効率が落ちない。

—

5. まとめ

Windows Formsにおける例外ハンドリングは、単なる「エラーメッセージの文字変更」ではない。
ユーザーの信頼を守り、システムの透明性を担保するための最重要アーキテクチャである。

  • UIスレッドは `Application.ThreadException` で優しく包み、継続の選択肢を与える。
  • バックグラウンドスレッドは `AppDomain.CurrentDomain.UnhandledException` で確実にキャッチしてログを残し、安全に散る。

この2つを正しく配置できた瞬間から、あなたの作るツールは「おもちゃのスクリプト」から「堅牢なエンタープライズ・アプリケーション」へと進化する。

現場のエンジニアよ、明日からのコードでぜひ実践してほしい。

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