【テクニカル・上級編】Windows Formsにおける非同期処理の例外伝播とUIスレッドへの安全な集約:AggregateExceptionの正しいハンドリング手順 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsにおける非同期処理の例外伝播とUIスレッドへの安全な集約:AggregateExceptionの正しいハンドリング手順

レガシーなVB6(VBA)システムからの脱却として構築されたWindows Forms(VB.NET)アプリケーションであっても、現代の非同期処理モデル(`Async` / `Await`)の導入は避けて通れない。

データベースへの非同期クエリ、外部REST APIとの通信、あるいは重負荷なバックグラウンド計算。これらをUIスレッドから切り離すことで、画面の「フリーズ(応答なし)」を防ぐことはアーキテクトとしての最低限の責務である。

しかし、ここで多くの開発者が罠に嵌まる。
「バックグラウンドスレッドで発生した例外が、UIスレッドに正しく伝播せず、プロセスをサイレントクラッシュさせる、あるいは宙に浮く」という現象だ。

今回は、`Task.Run`や非同期メソッドの内部で発生した例外がどのようにラップされ、いかにしてUIスレッドへ安全に集約されるべきか。そのメカニズムと、現場で即座に使える極限の設計パターンを解説する。

—

1. 非同期例外のメカニズム:なぜ例外は消えるのか?

VB.NETにおける`Task`ベースの非同期処理では、例外は即座のスローではなく、`Task`オブジェクトの内部状態(`Exception`プロパティ)に格納される。

ここで、`Async` / `Await`を使用していればコンパイラが自動的に`await`地点で`AggregateException`をアンラップし、元の例外(例: `SqlException`や`WebException`)として再スローしてくれる。

しかし、以下のアンチパターンを踏んだ瞬間、例外の連鎖は断ち切られる。

  • `Async Sub` の使用(イベントハンドラを除き、`Async Sub`は例外をキャッチする術がない)
  • `Task.Run` の中で `try-catch` を忘れる、あるいは `Wait()` や `.Result` によるデッドロックと例外の隠蔽
  • マルチタスク処理(`Task.WhenAll`等)における複数の例外の混在

特に `Task.WhenAll` や並列処理(`Parallel.For`など)では、複数の例外が同時に発生するため、それらは必ず `AggregateException` としてラップされる。これを適切に平坦化(Flatten)し、UIスレッド上で安全に処理しなければならない。

—

2. 実装パターン:安全な例外集約とUIスレッドへのMarshalling

以下のコードは、バックグラウンド処理中の例外を完全に捕捉し、`AggregateException` をアンラップした上で、WinFormsのUIスレッド(メインスレッド)へ安全にマーシャリングしてメッセージボックスを表示する堅牢な実装例である。

Imports System
Imports System.Threading.Tasks
Imports System.Windows.Forms

Public Class MainForm
Inherits Form

Private Async Sub btnExecute_Click(sender As Object, e As EventArgs) Handles btnExecute.Click
‘ 多重実行防止のためのロック
btnExecute.Enabled = False

Try
‘ 非同期処理の呼び出し
Await ExecuteHeavyBackgroundProcessAsync()

MessageBox.Show(“処理が正常に完了しました。”, “成功”, MessageBoxButtons.OK, MessageBoxIcon.Information)

Catch ex As AggregateException
‘ AggregateExceptionの場合、内部の例外をすべて精査・平坦化する
Dim flattenedEx = ex.Flatten()
Dim errorMessages As String = String.Empty

For Each innerEx As Exception In flattenedEx.InnerExceptions
‘ ログ出力基盤への書き込み(ここではDebug出力)
System.Diagnostics.Debug.WriteLine($”[Critical Error] {innerEx.GetType().Name}: {innerEx.Message}”)
errorMessages &= $”{innerEx.Message}{Environment.NewLine}”
.Next

MessageBox.Show($”以下のエラーが発生しました:{Environment.NewLine}{errorMessages}”,
“致命的なエラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)

Catch ex As Exception
‘ その他の予期せぬ単一例外
System.Diagnostics.Debug.WriteLine($”[Critical Error] {ex.GetType().Name}: {ex.Message}”)
MessageBox.Show($”予期せぬエラーが発生しました: {ex.Message}”,
“エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)

Finally
‘ UIの有効化(例外の有無に関わらず必ず実行)
If Not Me.IsDisposed Then
btnExecute.Enabled = True
End If
End Try
End Sub

”’

”’ 複数の非同期タスクを並行実行し、例外をあえて発生させるシミュレーション
”’

Private Async Function ExecuteHeavyBackgroundProcessAsync() As Task
‘ Task.Run内部での例外はTaskオブジェクトにキャプチャされる
Dim task1 As Task = Task.Run(Sub()
‘ 何らかの業務ロジックエラーを想定
Throw New InvalidOperationException(“データベース接続タイムアウトが発生しました。”)
End Sub)

Dim task2 As Task = Task.Run(Function() As Task
Return Task.Delay(500).ContinueWith(Sub(t)
Throw New ArgumentException(“パラメータ不正:IDが指定されていません。”)
End Sub)
End Function)

‘ 複数のタスクを同期・待機。ここで例外が発生した場合、AggregateExceptionとして集約される
Await Task.WhenAll(task1, task2)
End Function

End Class

—

3. シニアエンジニアが押さえるべきアーキテクチャの要点

上記のコードには、レガシーシステムを多数抱える現場のエンジニアだからこそ知っておくべき、いくつかの重要な防衛策が組み込まれている。

① `Async Sub` の原則禁止と例外の境界

イベントハンドラ(`Button_Click`など)を除き、戻り値のない非同期メソッドには必ず `Async Function … As Task` を使用すること。`Async Sub` は呼び出し元が完了をawaitできず、そこで発生した例外は `AppDomain.CurrentDomain.UnhandledException`(=プロセス強制終了)へと直行する。

② `AggregateException.Flatten()` の強制

`Task.WhenAll` やパラレル処理の裏側では、例外が入れ子(Nested)になることがある。`Flatten()` メソッドを呼び出すことで、階層構造になった例外ツリーを単一の平坦なリスト(`InnerExceptions`)に変換し、すべてのエラーメッセージを取りこぼすことなくユーザーに通知、あるいはログに記録できる。

③ UIスレッドの生存確認 (`IsDisposed`)

非同期処理が走っている最中に、ユーザーがウィンドウを閉じた場合、`Finally` ブロックやコールバック内でUIコントロール(`btnExecute.Enabled = true` など)にアクセスすると、`ObjectDisposedException` が誘発される。
`If Not Me.IsDisposed Then` というガード節は、非同期UIプログラミングにおける必須のイディオムである。

—

総括

Windows Formsにおける非同期処理は、UIの応答性を劇的に向上させる諸刃の剣である。スレッド境界を越える例外の挙動を理解せずして、安定したエンタープライズアプリケーションの構築はあり得ない。

「動けばいい」という実装から脱却し、`AggregateException` のハンドリングとスレッドセーフなライフサイクル管理を徹底すること。それこそが、レガシーとモダンをつなぐシニアエンジニアの技術的矜持である。

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