CodeGym /コース /C# SELF /いくつかの例外: AggregateExceptio...

いくつかの例外: AggregateException

C# SELF
レベル 61 , レッスン 1
使用可能

1. はじめに

想像してみてください: 非同期タスクを一度に10個走らせる — サイトをパースしたり、マーケットプレイスの価格をチェックしたり、大きな配列を計算したり。全部うまくスタートするけど、いくつかのタスクがエラーで落ちる: サーバーが応答しない場所があったり、データに問題があったり。同期コードなら1つの例外にしか遭遇しないこともありますが、非同期だとエラーが複数になることがあります。ここで登場するのが、すべてのエラーを「コンテナ」にまとめるAggregateExceptionです。

そもそもAggregateExceptionとは?

AggregateExceptionは、.NETにおける特殊な例外タイプで、並列/非同期操作(Task.WhenAllParallel.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 の挙動

タスクの待ち方 捕まえる例外
await Task.WhenAll()
最初の内部エラー
Task.WhenAll().Wait()
常に AggregateException
Task.WhenAll().Result
常に 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; // 他の例外は再スローされる
    });
}
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION