1. はじめに
想像してみてください: 非同期タスクを一度に10個走らせる — サイトをパースしたり、マーケットプレイスの価格をチェックしたり、大きな配列を計算したり。全部うまくスタートするけど、いくつかのタスクがエラーで落ちる: サーバーが応答しない場所があったり、データに問題があったり。同期コードなら1つの例外にしか遭遇しないこともありますが、非同期だとエラーが複数になることがあります。ここで登場するのが、すべてのエラーを「コンテナ」にまとめるAggregateExceptionです。
そもそもAggregateExceptionとは?
AggregateExceptionは、.NETにおける特殊な例外タイプで、並列/非同期操作(Task.WhenAll、Parallel.For、TPLなど)からのエラーを集約します。複数のタスクがエラーで終了すると、それらが1つのAggregateExceptionオブジェクトに“パッケージ”されます。
どこで発生するのか?
もっとも一般的なシナリオは、複数のタスクを
// 例 — 複数のタスクを並列で呼び出し、その一部が例外を投げる
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("オラー #1")),
Task.Run(() => throw new ArgumentException("オラー #2")),
Task.Run(() => { Console.WriteLine("タスク3は正常に完了しました!"); })
};
try
{
await Task.WhenAll(tasks); // await時にここでAggregateExceptionが展開される
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.GetType().Name} - {ex.Message}");
}
ただし注意点があります: await時にはAggregateExceptionが展開され、最初の内部例外が「そのまま」投げられます。一方、awaitを使わずにResultにアクセスしたり、Wait()を呼ぶと、実際にAggregateExceptionが返ってきます。
2. AggregateExceptionの見た目
この例外はInnerExceptionsというコレクションを持ち、そこにすべてのネストされたエラーが入ります:
try
{
Task task = Task.WhenAll(tasks);
task.Wait(); // ここではエラーがあればAggregateExceptionになる
}
catch (AggregateException aggEx)
{
Console.WriteLine($"エラーの合計: {aggEx.InnerExceptions.Count}");
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"型: {e.GetType().Name}, メッセージ: {e.Message}");
}
}
出力例:
エラーの合計: 2
型: InvalidOperationException, メッセージ: オラー #1
型: ArgumentException, メッセージ: オラー #2
AggregateExceptionはどうやって発生するのか
+--------------------------+
| いくつかの Task を起動 |
+-----+---------+----------+
| |
v v
Task1 Task2 ... TaskN
| |
| (エラーで落ちる)
|-------------------+
| v
| Exception2
v
Exception1
|
v
+---------------------------------+
| Task.WhenAll または Parallel.For |
| (すべての例外を収集) |
+---------------------------------+
|
v
AggregateException
(InnerExceptions: すべてのエラー)
なぜ awaitだとAggregateExceptionが見えないのか?
await時、.NETはAggregateExceptionを“展開”して最初の内部例外(InnerExceptions[0])を投げます。
// InvalidOperationExceptionを投げる単一のタスクの例
try
{
await Task.Run(() => throw new InvalidOperationException("エラー!"));
}
catch (Exception ex)
{
// ここでexはAggregateExceptionではなくInvalidOperationExceptionそのもの
Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}
すべてのエラーにアクセスしたいなら、Wait()やResultを使ってください(ただしスレッドがブロックされる点には注意)。
3. すべての例外をキャッチして処理する
理論を実践につなげます。複数のタスクがユーザーデータを処理していて、すべての失敗をログに残したい場合を想定します。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApp
{
class Program
{
static async Task Main(string[] args)
{
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("不正な操作!")),
Task.Run(() => throw new DivideByZeroException("ゼロ除算!")),
Task.Run(() => Console.WriteLine("3番目のタスクは成功しました"))
};
try
{
// タスクを非同期に待つ — 最初の内部例外を受け取る
Task allTasks = Task.WhenAll(tasks);
await allTasks; // await時にAggregateExceptionは展開される
}
catch (Exception ex)
{
// awaitでは最初の内部例外しか見えない
Console.WriteLine($"最初のエラー: {ex.GetType().Name} - {ex.Message}");
// もしこれがAggregateExceptionなら、全てを順に見ることもできる
if (ex is AggregateException aggEx)
{
foreach (var inner in aggEx.InnerExceptions)
{
Console.WriteLine($"一覧からのエラー: {inner.GetType().Name} - {inner.Message}");
}
}
}
// 同期的にすべてのエラーを捕まえる例
try
{
var allTasks = Task.WhenAll(tasks);
allTasks.Wait(); // スレッドをブロックする — UIではやらないでください!
}
catch (AggregateException aggEx)
{
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"[Wait] エラー: {e.GetType().Name} — {e.Message}");
}
}
}
}
}
awaitを使ったtry/catchでは最初のエラーしか見えません。Wait()だと、すべての内包されたエラーを持つ古典的なAggregateExceptionが来ます。
4. 大量のエラーの処理
実用的なアプリケーションでは、多数のエラーをそれぞれ別に処理する必要がよくあります。たとえば、全ユーザーにメールを送っていて一部の送信が失敗するケース:
var sendTasks = emailList.Select(email => Task.Run(() => SendEmail(email))).ToList();
try
{
Task.WaitAll(sendTasks.ToArray());
}
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.InnerExceptions)
{
if (ex is SmtpException)
Console.WriteLine("メール送信エラー: " + ex.Message);
else
Console.WriteLine("不明なエラー: " + ex.Message);
}
}
こうすることで、各失敗をログに残し、個別の対処ができます。
5. 役に立つ注意点
この挙動の理由
.NETは利便性を重視しています: UIでは通常最初のエラーだけで十分なことが多い(await)。しかしサーバー側や、すべての失敗を処理する必要があるシステムでは、AggregateExceptionが欠かせません。
パターン: Flatten — エラーのリストをフラットにする
メソッドFlatten()は、ネストされたAggregateExceptionを平坦なリストに変換します:
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.Flatten().InnerExceptions)
{
Console.WriteLine($"展開されたエラー: {ex.GetType().Name} — {ex.Message}");
}
}
待ち方による Task の挙動
| タスクの待ち方 | 捕まえる例外 |
|---|---|
|
最初の内部エラー |
|
常に AggregateException |
|
常に AggregateException |
| 個別の await | タスクが投げた例外、またはタスクが AggregateExceptionで終わった場合は最初の内部エラー |
覚えておくべきこと
- AggregateExceptionは、多数のエラーが同時に起きうるシナリオで有用です。
- awaitでは最初のエラーを捕まえるのが便利で、同期的待機ではすべてのInnerExceptionsを巡回します。
- 丁寧に処理するためにFlatten()やHandle()を使いましょう。
- UIで同期的と非同期的な待ち方を混ぜるとフリーズやデッドロックになります。
7. AggregateExceptionの処理でありがちなミス
誤解: await Task.WhenAllは常にAggregateExceptionを投げる、というもの。実際には違い、例外は展開されて最初の内部例外が見えます。
ResultにアクセスしたりWait()を呼ぶと、タスク完了までスレッドがブロックされます。UIだとインターフェースが固まります。
「一部のエラーだけ静かに処理したい」場合は、Handle()を使ってください:
catch (AggregateException aggEx)
{
aggEx.Handle(ex =>
{
if (ex is InvalidOperationException)
{
// このエラーは処理したので、再スローしない
return true;
}
return false; // 他の例外は再スローされる
});
}
GO TO FULL VERSION