CodeGym /課程 /C# SELF /多重例外: AggregateException

多重例外: AggregateException

C# SELF
等級 61, 課堂 1
開放

1. 介紹

想像一下:你同時啟動好幾個非同步任務——爬網站、查商城價格或計算大型資料陣列。所有任務看起來都啟動了,但有幾個任務發生錯誤:有的伺服器沒回應,有的是資料有問題。在同步程式碼中你可能只看到一個例外,但在非同步情況下可能會有好幾個錯誤。這時候就輪到 AggregateException 上場——它把所有錯誤收進同一個「容器」。

什麼是 AggregateException?

AggregateException 是 .NET 裡一種特殊的例外型別,用來聚合來自平行/非同步操作的錯誤:像是 Task.WhenAll, Parallel.For, TPL 等等。當兩個或以上任務以錯誤結束時,這些錯誤會「打包」到同一個 AggregateException 物件裡。

AggregateException 在哪裡會出現?

最常見的場景就是透過 Task.WhenAll 等待多個任務:

// 示例 — 平行呼叫好幾個任務,其中部分會拋出例外
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 就是 InvalidOperationException,而不是 AggregateException
    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("第三個任務成功通過"))
            };

            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 是不可或缺的。

Pattern: 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 中混用同步與非同步等待:會造成界面卡頓或 deadlock。

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