• システム開発に関わる内容をざっくりと書いていく

C# AggregateException のほどき方(Flatten / InnerExceptions)

AggregateException は「複数の例外をまとめた袋」。async / Task.WhenAll / Parallel でよく出る。中身をほどかないと本当の原因が見えない。


先に結論

  • 見るべきは外側ではなく InnerExceptions(複数)または InnerException
  • ネストしているときは Flatten() してから列挙
  • await した例外は多くの場合「中身の例外」がそのまま再スローされる(Aggregate に包まれにくい)
  • task.Result / task.Wait() / Task.WaitAll だと Aggregate になりやすい

いつ出現するか

  • Task.WhenAll のあとに .Wait() / .Result
  • Parallel.For / PLINQ の集約例外
  • 古いコードの task.Wait()
  • 一部ライブラリが複数失敗をまとめて投げたとき

ほどき方

try
{
    Task.WaitAll(tasks);
}
catch (AggregateException ex)
{
    foreach (var e in ex.Flatten().InnerExceptions)
    {
        // 実際の原因(HttpRequestException など)をログ
        Console.WriteLine(e.GetType().Name + ": " + e.Message);
    }
}
  • Flatten() … 入れ子の Aggregate を平らにする
  • InnerExceptions … 平らにしたあとの個別例外
  • ログには外側メッセージだけでなく 各 Inner の型と StackTrace を残す

async ではどう書くか

// 推奨: await(1件失敗でもその例外が見えやすい)
try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    // WhenAll は「最初に観測した例外」が来ることも。
    // 全失敗を見たいなら tasks を個別に確認する
}

// 全タスクの例外を拾う例
var results = await Task.WhenAll(
    tasks.Select(async t =>
    {
        try { await t; return (Exception?)null; }
        catch (Exception e) { return e; }
    }));

ポイント: ブロッキング(Wait/Result)をやめて await すると、Aggregate とデッドロックの両方を減らせる。

よくあるハマり

  • 外側だけ catch して「AggregateException が出た」で終わる → 原因不明のまま
  • InnerException だけ見て 2 件目以降を落とす
  • Flatten せずにネストを追う

関連: async で待ち続けるときの見方

ざっくりまとめ

  • Aggregate = 袋。中を Flatten().InnerExceptions で見る
  • Wait/Result より await
  • ログは個別例外の型まで残す

関連記事