こんにちは! Windows Formsアプリケーションの開発、日夜お疲れ様です。
マクロの記録や古いVBScriptの世界から一歩踏み出し、本格的なVisual Basic(特に.NETの世界)でUIを作り始めると、避けて通れないのが「非同期処理(Async / Await)」と「例外処理(エラーハンドリング)」の壁です。
「ボタンを押したら裏で重い処理を動かしたい(UIを固めたくない)」と思って `Async` や `Task.Run` を使ってみたものの、なぜかアプリが突然フリーズしたり、原因不明のクラッシュを起こしたりして頭を抱えていませんか?
今回は、非同期処理の裏側でこっそり起きている例外(エラー)がどう伝播し、なぜアプリをクラッシュさせるのか、そしてそれを完全に手なずけて安全にユーザーに伝えるための極意を、優しく、かつプロのアーキテクトの視点から紐解いていきます。
ここをクリアすれば、あなたの書くWindows Formsアプリは見違えるほど堅牢になりますよ。さあ、一緒にマスターしていきましょう!
—
1. なぜ非同期処理の例外はアプリをクラッシュさせるのか?
まずは、Windows Formsの心臓部である「UIスレッド」と、非同期処理が走る「別スレッド(ワーカースレッド)」の関係図を見てみましょう。
[ ユーザーの操作 ]
│
▼
┌───────────┐ Async / Await ┌─────────────────┐
│ UIスレッド │ ──────────────────────> │ ワーカースレッド │
│ (画面描画) │ │ (重い処理を実行) │
└───────────┘ └───────────┬─────┘
▲ │
│ 例外(エラー)が発生! ▼
│ (ここでキャッチし忘れると…) [ 💥 予期せぬ例外 ]
│ │
│ .NET 4.0〜 ──> TaskScheduler.UnobservedTaskException
│ .NET 4.5+ ──> アプリケーション全体が即死クラッシュ!
古い時代のVB.NETやVBAでは、エラーは「起きたその場所」で止まりました。しかし、現代の `.NET` におけるタスク(`Task`)の世界では、別スレッドで起きた例外は親スレッドへ「伝播」しようとします。
ここで `Try…Catch` の網をくぐり抜けた例外は、`.NET Framework / .NET Core` の仕様により、アプリケーション全体の強制終了(クラッシュ)を引き起こします。「えっ、ちゃんとTry Catchしたつもりなのに!」という罠がここに潜んでいるのです。
—
2. 複数の例外を包み込む「AggregateException」の正体
さらに話は複雑になります。`Task.WhenAll` などを使って複数の非同期処理を同時に走らせた場合、複数のスレッドで同時にエラーが起きることがあります。
このとき、 .NETは複数の例外をまとめて1つの袋に詰め込みます。これが `AggregateException`(集約例外) です。
- 普通の例外 (`Exception`): 個別のエラー(例:ファイルがない、DBがつながらない)
- 集約例外 (`AggregateException`): エラーの詰め合わせパック。中に複数の例外(`InnerExceptions`)がゴロゴロ入っている。
VB.NETで非同期処理を扱うときは、この「詰め合わせパック」を開封して、中身を取り出すスマートな手順が必要になります。
—
3. 【実践】安全に例外をキャッチしてUIに伝える設計パターン
百聞は一見に如かず。Windows Formsのフォーム上に配置したボタン(`Button1`)を起点として、安全に非同期処理を行い、エラーが発生した場合はクラッシュを防いでメッセージボックスで優しく教えるコードを見てみましょう。
以下のコードを、そのままあなたのフォームクラスに貼り付けてみてください。
Imports System.Threading.Tasks
Imports System.Windows.Forms
Public Class Form1
‘ ボタンクリックイベント(Asyncを付与して非同期メソッドにする)
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
‘ 1. ユーザー操作を防ぐためにボタンを無効化(二重実行防止)
Button1.Enabled = False
Try
‘ 2. 重い処理(または失敗するかもしれない処理)を別スレッドで実行
Await Task.Run(
Sub()
‘ ここであえてエラーを発生させてみます(例:ゼロ除算)
Dim x As Integer = 0
Dim y As Integer = 10 / x
End Sub)
‘ 成功時の処理
MessageBox.Show(“処理が正常に完了しました!”, “成功”, MessageBoxButtons.OK, MessageBoxIcon.Information)
Catch ex As AggregateException
‘ 3. 複数タスクの並列実行などで起きた「例外の詰め合わせパック」を処理
‘ Flatten()を使うことで、ネストしたAggregateExceptionを綺麗に1枚の平らなリストにします
Dim flattenedEx = ex.Flatten()
‘ ユーザー向けに分かりやすいエラーメッセージを構築
Dim errorMessages As String = “以下のエラーが発生しました:” & vbCrLf
For Each inner As Exception In flattenedEx.InnerExceptions
errorMessages & = “・” & inner.Message & vbCrLf
Next
‘ UIスレッドから安全にモーダルダイアログを表示
MessageBox.Show(errorMessages, “処理エラー (複数)”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Catch ex As Exception
‘ 4. 通常の単一の非同期例外のキャッチ
‘ Awaitを使っている場合、ここでTask内の例外が美しくキャッチされます!
MessageBox.Show($”処理中にエラーが発生しました。{vbCrLf}{ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
‘ 5. 成功しようが失敗しようが、必ずUIの状態を元に戻す
Button1.Enabled = True
End If
End Sub
End Class
—
4. コードのここがポイント! 押さえておきたい3つの極意
上記のコードには、プロのエンジニアが現場で必ず使う「安全なUI設計の知見」が詰まっています。
① `Await` キーワードが例外を「アンラップ」してくれる
`Await Task.Run(…)` と書くことで、別スレッド(ワーカースレッド)で発生した例外は、まるでその場で起きたかのように `Catch ex As Exception` に吸い上げられます。`Await` は単に処理を待つだけでなく、スレッド間の例外の橋渡しをする魔法の構文なのです。
② `AggregateException.Flatten()` でエラーの取りこぼしをゼロに
もし将来的に `Task.WhenAll` などを使って「複数のファイルを同時にダウンロードする処理」などに拡張したとき、複数のエラーが同時に発生する可能性があります。`ex.Flatten()` を挟んでおくことで、複雑に絡み合ったエラーの階層をフラットにし、全ての原因をもれなくユーザーに伝えることができます。
③ `Finally` ブロックでUIのデッドロック・操作不能を防ぐ
非同期処理で最も多いバグが、「エラーが起きた後にボタンが押せないまま固まる」という現象です。`Try…Catch` のあとに必ず `Finally` を置き、そこでボタンの有効化(`Button1.Enabled = True`)を行うことで、どんな結末を迎えてもUIが必ず安全な状態に復帰します。
—
まとめ:非同期処理の例外制御は「優しさ」の形
Visual BasicでのWindows Forms開発は、歴史が長い分、古い書き方と新しい非同期の書き方が混ざりやすい領域です。
しかし、今回紹介した 「`Async/Await` による例外のキャッチ」「`AggregateException.Flatten()` による複数エラーの分解」「`Finally` によるUIの確実な復帰」 の3ステップを押さえておけば、もう予期せぬクラッシュに怯える必要はありません。
安定して動作し、万が一エラーが起きても親切にユーザーを導いてくれるUI――それこそが、優れたエンジニアが作るアプリケーションの証です。
ここをクリアしたあなたなら、もうVB.NETの非同期処理で迷うことはありません。自信を持って、次の機能開発へ進んでくださいね!
