在深海裡,章魚的八隻腕足總是同時探向不同的方向,纏繞著礁岩、觸碰著漩渦,也悄悄在黑暗中編織出一張未被看見的網。我們的非同步程式,就像這隻章魚,當你派出無數任務游向未知,若沒有好好等待(await),有些錯誤就會像溜走的墨汁般散開,再也無法追溯

若選擇用同步阻塞強行捕捉,例外也會被包裹成難解的聚合結,像是八爪同時纏住獵物卻黏成一團。學會如何等待,如何攤開多執行緒裡潛藏的觸手,才是馴服這隻章魚、看清每一道潛伏例外的唯一方法。
🐙

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static async Task ExceptionResultTest() { try { TaskThatThrowsExceptionStringAsync(); } catch(Exception ex) { throw; } }
private static async Task<string> TaskThatThrowsExceptionStringAsync() { throw new NotImplementedException("內部錯誤#####################################"); }
|
執行後什麼都沒有。錯誤直接遺失,因為 try…catch 並沒有真正等待這個 Task 的完成。async 方法呼叫後立即傳回 Task。try 區塊在呼叫後就結束了,例外發生時已離開 try 區塊。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static async Task<string> ExceptionResultTest() { try { return TaskThatThrowsExceptionStringAsync().Result; } catch(Exception ex) { throw; } }
private static async Task<string> TaskThatThrowsExceptionStringAsync() { throw new NotImplementedException("內部錯誤#####################################"); }
|
成功抓到錯誤。但訊息是 AggregateException: One or more errors occurred。需要看 InnerException 才知道真正錯誤。.Result 與 .Wait() 是同步阻塞。若 Task 出現例外,CLR 會將例外包成 AggregateException。這是 .NET 的多任務例外聚合機制

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static async Task<string> ExceptionResultTest() { try { return await TaskThatThrowsExceptionStringAsync(); } catch(Exception ex) { throw; } }
private static async Task<string> TaskThatThrowsExceptionStringAsync() { throw new NotImplementedException("內部錯誤#####################################"); }
|
直接捕捉到 NotImplementedException。訊息清楚明確,堆疊追蹤與除錯資訊完整。await 會將 Task 中發生的例外重新拋出(unwrap)。因此例外型態不被包成 AggregateException,除錯體驗佳

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static async Task ExceptionResultTest() { try { await TaskThatThrowsExceptionStringAsync(); } catch(Exception ex) { throw; } }
private static async Task TaskThatThrowsExceptionStringAsync() { throw new NotImplementedException("內部錯誤#####################################"); }
|
await 發現 Task 狀態 = Faulted → 把裡面的 Exception 拿出來,catch 成功攔到例外 , throw 表示保留原本的 stack trace,原封不動往上丟

如果一次執行多個 Task,用 .WaitAll() 或 .Wait(),所有例外都會被聚合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public static async Task ExceptionMultipleResultTest() { try { MultipleTasks().Wait(); } catch (AggregateException ex) { foreach (var inner in ex.InnerExceptions) { Console.WriteLine($"==============={inner.Message}====================="); } throw; } }
private static async Task MultipleTasks() { var tasks = new List<Task> { Task.Run(() => throw new InvalidOperationException("錯誤 A")), Task.Run(() => throw new NotImplementedException("錯誤 B")) };
await Task.WhenAll(tasks); }
|
若用 await Task.WhenAll(),則第一個出現的例外就會被拋出,且同樣是原型別,不是 AggregateException

- 非同步永遠要 await, 除非有特別需求(如 Fire-and-Forget),否則不要只是丟出 Task
- 同時多任務,盡量用 await Task.WhenAll, 不要用 .WaitAll(),除非需要同步阻塞
- 理解聚合例外的機制, .Wait() 與 .Result 的 AggregateException 並非 bug,而是多執行緒任務例外安全聚合機制
- 在 Library 或 API 層面提供非同步介面, 讓呼叫端決定是否要 await,保留彈性
