1. 非同期メソッドと例外
コードで何か悪いこと(例えばゼロ除算や存在しないファイルへのアクセス)が起きると例外が投げられ、通常は try-catch で捕まえます。順次かつ単一スレッドで実行される間はそれで簡単です。しかし非同期が入ると状況は宇宙空間みたいになり、例外は期待している場所とは遠くで「発生」したり、まったく見逃されたりします。
理由は、非同期メソッドはたいていタスク(Task)を返し、メソッドから抜けた後も実行が続くことがあるからです。例外はメインスレッドが「実行を手放した」後に発生するかもしれません。だから非同期メソッドの呼び出しを囲む従来の try-catch は、同期コードのときと同じようには機能しないことがあります。
簡単な例で見てみましょう。ミニアプリに次のような非同期メソッドがあるとします:
// フラグメント: 非同期の「レポート送信」計算
public async Task SendReportAsync()
{
// ここにはネットワーク呼び出しやファイルアクセスがあるかもしれない
await Task.Delay(100);
throw new InvalidOperationException("レポート送信中のエラー!");
}
呼び出し方はこうです:
SendReportAsync();
Console.WriteLine("処理を続けます...");
ビジュアル化
flowchart TD
Start["メイン スレッド"]
Call[/"SendReportAsync() の呼び出し"/]
Continue["処理は続く..."]
Exception["Task 内で例外が発生"]
Unhandled["例外が未処理のまま!"]
Start --> Call --> Continue
Call -.- Exception --> Unhandled
結論: 非同期メソッドが Task を返し、タスクの完了を待たない(await か .Wait() を使わない)と、例外は「見逃され」ます。最良の場合はランタイムが "Task 内の未処理例外" 的なログを出します。最悪の場合はエラーを完全に見失い、原因不明のバグを長時間探す羽目になります。
2. 非同期コードで例外を正しく捕まえるには?
await + try-catch を使う
正しいやり方を見てみましょう:
try
{
await SendReportAsync(); // Task の完了を待つ
Console.WriteLine("レポートは正常に送信されました!");
}
catch (Exception ex)
{
Console.WriteLine($"おっと! 何か問題が発生しました: {ex.Message}");
}
どう動くか? 呼び出しの前に await を置くと、C# はあなたのメソッドを await の前後で分割します。非同期部分で例外が起きると、それは await を置いた場所で再スローされ、通常の try-catch で捕まえられます。
アプリ向けの例
デモにエラーハンドリングを追加します:
public async Task StartReportProcessAsync()
{
try
{
await SendReportAsync();
Console.WriteLine("レポートは正常に送信されました!");
}
catch (Exception ex)
{
Console.WriteLine($"レポート送信中のエラー: {ex.Message}");
}
}
呼び出し:
await StartReportProcessAsync();
.Wait(), .Result — コンソールでは最悪ではないが推奨されない手段
特にコンソールアプリでは、トップレベルで await を使えないことがあります(古い C#、Main メソッド)。その場合は同期的にタスクを待つために .Wait() や .Result を使うことになります。
try
{
SendReportAsync().Wait();
}
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.InnerExceptions)
Console.WriteLine($"エラー: {ex.Message}");
}
なぜか? .Wait() と .Result は元の例外を常に AggregateException でラップします。これは一つ以上の例外を含められるコンテナです。内部に一つまたは複数の例外があるかもしれないので、ループで分解する必要があります。AggregateException の詳細は 公式ドキュメント を参照してください。
重要!
最新の .NET(C# 7.1 以降)では、非同期の Main を宣言してエントリポイントで直接 await を使えます:
static async Task Main(string[] args)
{
await StartReportProcessAsync();
}
3. "fire-and-forget" タスクの例外
非同期メソッドを起動して完了を待たず、タスクへの参照も保持しなかったら何が起きるでしょう?
SendReportAsync(); // タスクを「忘れた」
この場合の問題は、タスク内で発生した例外が誰にも処理されないことです。環境や設定によってはアプリがクラッシュすることもあれば、単に警告がログに残るだけのこともあります。これは C# のバグではなく、タスクの動作の論理的帰結です。
正しいやり方は?
- 理想的には、タスクが重大な失敗で終了する可能性があるなら "fire-and-forget" を避ける。
- どうしても "fire-and-forget" にするなら、メソッド内部で明示的にエラー処理を行う。
public async Task SendReportSafeAsync()
{
try
{
await Task.Delay(100);
throw new InvalidOperationException("送信中のエラー!");
}
catch (Exception ex)
{
// ログ記録やエラーハンドリング
Console.WriteLine($"[ログ] 例外: {ex.Message}");
}
}
// 呼び出し
SendReportSafeAsync();
要点: タスクを追跡せず await もしない場合、非同期メソッドの本体を必ず try-catch で包み、少なくともログを残すようにしましょう。そうすればエラーを失わずに済みます。
4. 並列タスクと例外: Task.WhenAll 他
実アプリでは複数の独立した非同期タスクを同時に走らせ、すべての完了を待ちたいことがよくあります。例えば複数の宛先に並列でレポートを送る場合:
var tasks = new List<Task>
{
SendReportAsync(),
SendReportAsync(),
SendReportAsync()
};
await Task.WhenAll(tasks);
1つ以上のタスクが例外を投げたら何が起きるか?
こうやって捕まえる
await で Task.WhenAll(tasks) を待つと、少なくとも一つのタスクがエラーで終了していれば、await は最初にエラーで終わったタスクの例外を再スローします(通常は AggregateException でラップされません)。
ただし注意: 複数のタスクが失敗している場合、結果として AggregateException が投げられ、内部に複数の例外が含まれることがあります。
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// もし AggregateException なら分解する
if (ex is AggregateException agg)
{
foreach (var inner in agg.InnerExceptions)
Console.WriteLine($"タスク内のエラー: {inner.Message}");
}
else
{
Console.WriteLine($"エラー: {ex.Message}");
}
}
単一タスクを await する場合、通常は例外は AggregateException にラップされません。ただし WhenAll のような場合は複数例外を扱う必要があります!
5. 非同期デリゲートとエラーハンドリング
WPF、WinForms、ASP.NET のような UI アプリケーションでは、イベントハンドラを非同期ラムダで書くことがよくあります。もしハンドラ内の例外が外に出ると、UI フレームワーク次第でアプリがクラッシュしたり、エラーが握りつぶされたりします。
推奨
非同期デリゲートの内部では常に try-catch を使いましょう:
button.Click += async (sender, args) =>
{
try
{
await SendReportAsync();
}
catch (Exception ex)
{
MessageBox.Show($"エラー: {ex.Message}");
}
};
GO TO FULL VERSION