【入門編】Windows Formsにおけるマルチスレッドでの例外捕捉:Application.ThreadExceptionとAppDomain.UnhandledExceptionを連携させたクラッシュレポート自動送信システムの構築 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

突然の強制終了にさよならを。WinFormsにおける「最強の例外捕捉システム」の構築術

「さっきまで動いていたのに、急にアプリが消えた……」
「ユーザーから『動かない』と言われたけれど、エラーメッセージすら出ずに終了してしまう……」

Windows Forms(WinForms)でアプリケーションを開発していると、一度はこのような「音も立てずに消えるアプリ」の恐怖に直面したことがあるはずです。

マクロの記録から一歩踏み出し、本格的な業務アプリケーションを作ろうとする時、避けて通れないのが「例外(エラー)への対処」です。多くの人は `Try…Catch` を使ってエラーを防ごうとしますが、実はそれだけでは不十分なのです。なぜなら、マルチスレッドの世界では、`Try…Catch` の網をすり抜けてアプリをクラッシュさせる「見えないエラー」が潜んでいるからです。

今日は、プロの開発現場で欠かせない「クラッシュレポート自動送信システム」の考え方と、その実装方法を伝授します。これをマスターすれば、あなたは「エラーが起きたら、なぜ起きたのかを即座に把握できる」エンジニアへと進化できます。

さあ、一緒に極限の防御策を構築していきましょう。

1. なぜ `Try…Catch` だけでは足りないのか?

まず、ここが一番の勘所です。

通常のプログラミングでは、エラーが起きそうな場所に `Try…Catch` を書きます。しかし、WinFormsアプリケーションには「エラーが発生する場所」が主に2つのレイヤーに分かれていることを忘れてはいけません。

1. UIスレッド(メインスレッド): ボタンのクリックや画面の描画など、ユーザーが直接触れる部分。
2. バックグラウンドスレッド: 重い処理や通信など、画面を固めないために裏側で動かしている部分。

もし、バックグラウンドスレッド内でエラーが発生し、それが適切にキャッチされていない場合、`Try…Catch` をいくら書いてもアプリ全体が強制終了してしまいます。 この「網の目から漏れたエラー」を捕まえるための仕組みこそが、今回学ぶテーマなのです。

2. 二つの守護神:`ThreadException` と `UnhandledException`

アプリを全方位から守るためには、性質の異なる2つのイベントハンドラを連携させる必要があります。

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

これは、「UIスレッド(メインの画面操作)」で発生した未処理の例外をキャッチするためのものです。
これを使えば、「エラーが発生しました。詳細を送信します」といったメッセージボックスをユーザーに表示し、アプリが突然消えるのを防ぐ(あるいは制御する)ことができます。

② `AppDomain.UnhandledException`(最後の砦)

これは、「それ以外のすべて」、つまりバックグラウンドスレッドで発生したエラーや、アプリの基盤そのもので起きたエラーをキャッチするためのものです。
これは「アプリを救う」ためのものではなく、「アプリが死ぬ直前に、何が起きたのかを記録する(ブラックボックスの回収)」ためのものです。

この2つを組み合わせることで、アプリのどこで火の手が上がっても、確実にログを残せるようになります。

3. 実装:クラッシュレポート自動送信システムの設計図

それでは、実際に動くコードを見ていきましょう。
ポイントは、アプリケーションの開始地点である `Sub Main` をカスタマイズすることです。

プロジェクトの設定変更

このコードを動かすには、プロジェクトのプロパティで「スタートアップ オブジェクト」を `Sub Main` を含むモジュールに設定する必要があります。

''' <summary>
''' 究極の例外ハンドリング・システム
''' </summary>
Imports System.IO
Imports System.Text
Imports System.Windows.Forms

Module Program
    ''' <summary>
    ''' アプリケーションのエントリーポイント
    ''' </summary>
    Sub Main()
        ' 1. UIスレッドの例外をキャッチするモードを設定
        ' これを設定しないと、ThreadExceptionが正しく動作しない場合があります
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)

        ' 2. UIスレッドの例外ハンドラを登録
        AddHandler Application.ThreadException, AddressOf HandleUIException

        ' 3. 全スレッド(AppDomain)の未処理例外ハンドラを登録
        AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf HandleAppDomainException

        ' 通常のアプリケーション起動
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        
        ' メインフォームの起動
        Application.Run(New MainForm())
    End Sub

    ''' <summary>
    ''' UIスレッドで発生した例外を処理する
    ''' </summary>
    ''' <param name="ex">発生した例外</param>
    Private Sub HandleUIException(sender As Object, e As ThreadExceptionEventArgs)
        ' e.Exception にエラーの詳細が入っています
        Dim ex As Exception = e.Exception
        
        ' ユーザーに通知(ここでアプリを落とさず、ログだけ残して継続させることも可能)
        MessageBox.Show("予期せぬエラーが発生しました。開発チームへ報告します。" & vbCrLf & ex.Message, 
                        "システムエラー", MessageBoxButtons.OK, MessageBoxIcon.Error)

        ' ログ出力
        WriteCrashLog(ex, "UI_THREAD_ERROR")
    End Sub

    ''' <summary>
    ''' バックグラウンドスレッド等、アプリ全体で発生した未処理の例外を処理する
    ''' </summary>
    ''' <param name="sender">ソース</param>
    ''' <param name="e">例外情報</param>
    Private Sub HandleAppDomainException(sender As Object, e As UnhandledExceptionEventArgs)
        ' e.ExceptionObject は Object 型なので Exception にキャストが必要
        Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
        
        ' このイベントが呼ばれたとき、アプリは既に終了プロセスに入っています。
        ' ここでは「いかに確実にログを残すか」だけに集中します。
        WriteCrashLog(ex, "APP_DOMAIN_FATAL_ERROR")
    End Sub

    ''' <summary>
    ''' クラッシュログをファイルに書き出す(実際はここでサーバー送信も行う)
    ''' </summary>
    ''' <param name="ex">例外</param>
    ''' <param name="errorType">エラーの種類</param>
    Private Sub WriteCrashLog(ex As Exception, errorType As String)
        Try
            Dim logPath As String = Path.Combine(Application.StartupPath, "crash_log.txt")
            Dim sb As New StringBuilder()

            sb.AppendLine("==================================================")
            sb.AppendLine($"[ERROR TYPE]: {errorType}")
            sb.AppendLine($"[TIME]: {DateTime.Now:yyyy/MM/dd HH:mm:ss}")
            sb.AppendLine($"[OS]: {Environment.OSVersion}")
            sb.AppendLine($"[MACHINE]: {Environment.MachineName}")
            sb.AppendLine($"[USER]: {Environment.UserName}")
            sb.AppendLine("--------------------------------------------------")
            
            If ex IsNot Nothing Then
                ' ToString() はスタックトレース(エラーの発生箇所)を含んでいるので非常に重要!
                sb.AppendLine("Exception Details:")
                sb.AppendLine(ex.ToString())
            Else
                sb.AppendLine("Unknown Exception occurred.")
            End If
            
            sb.AppendLine("==================================================")
            sb.AppendLine()

            ' ファイルへの書き込み(Append: 追記モード)
            File.AppendAllText(logPath, sb.ToString())

            ' 【応用】ここで、HTTP Clientなどを使って社内サーバーへ
            ' JSON形式などでPOST送信する処理を記述すると、保守性は極限まで高まります。

        Catch
            ' ログ出力自体が失敗した場合は、何もできない(無限ループ防止のため)
        End Try
    End Sub
End Module

4. ここがプロのテクニック! 3つの重要ポイント

コードを書いた後、以下の点に注目してください。これを知っているだけで、あなたの設計思想は「初学者」から「エンジニア」へと変わります。

① `Exception.ToString()` の重みを知る

コード内で `ex.Message` だけでなく `ex.ToString()` を使っていますね。
`Message` は「何が起きたか」しか教えてくれませんが、`ToString()` は「どのファイルの何行目で、どのメソッドを経由してエラーが起きたか(スタックトレース)」をすべて含んでいます。これがないと、デバッグは不可能です。

② ログ出力の「自己完結性」

`WriteCrashLog` メソッドの中で、`Try…Catch` をもう一度使っています。
もし「ログを書き込もうとしてディスク容量不足でエラーになった」場合、再び例外が発生して無限ループに陥り、アプリが完全にフリーズしてしまいます。「エラーを記録する処理自体は、絶対にエラーで止まらないように作る」。これが鉄則です。

③ 環境情報の収集

`Environment.OSVersion` や `Environment.MachineName` を取得しています。
「自分のPCでは動くのに、ユーザーのPCでは動かない」という現象は、WinForms開発における最大の敵です。OSのバージョンやユーザー環境をログに含めることで、再現性を劇的に高めることができます。

まとめ:エラーは「敵」ではなく「情報」である

エラーが発生することは、決して恥ずかしいことではありません。むしろ、エラーが起きたときに「なぜ起きたのか」を即座に、正確に把握できる仕組みを持っていることこそが、一流のエンジニアの証です。

今回紹介した `Application.ThreadException` と `AppDomain.UnhandledException` の連携。これを実装するだけで、あなたのアプリケーションの信頼性は格段に向上します。

「ここをクリアすれば、Visual Basic (VB / VB.NET)の基本はバッチリですよ」

自信を持って、次のステップへ進んでください。応援しています!

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