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 的行為
| 如何等待任務 | 會捕到什麼例外 |
|---|---|
|
第一個內部錯誤 |
|
一定是 AggregateException |
|
一定是 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; // 其他例外會被重新拋出
});
}
GO TO FULL VERSION