Async

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

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

🐙


🎵 實驗一:沒 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 區塊。



🎵 實驗二:用 .Result 或 .Wait() 阻塞取得結果

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 的多任務例外聚合機制。



🎵 實驗三:使用 await 等待結果

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,除錯體驗佳。



🎵 實驗四:Task 不回傳值也一樣

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("內部錯誤#####################################");
}



🎵 多個 Task 的例外處理

如果一次執行多個 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,保留彈性。



🐙 結語

章魚從不害怕放開八爪去探索,它知道每一條觸手都可能捕捉到不同的訊息,也可能同時失落在暗潮裡。
非同步程式也是如此,Task 被派遣到多執行緒的深海中,若沒有耐心等待(await),有些錯誤終將像失去的墨汁,永遠融進黑暗裡。

當你學會在適當的時機攤開每一隻觸手、用 await 解開包裹的聚合結,就能在錯誤發生時看見最真實的輪廓,讓除錯不再是混沌的漩渦,而是一場與章魚共舞的優雅對話。
願你的程式,也能像這隻八爪章魚一樣,靈活捕捉每個潛伏的風暴,帶著錯誤浮出水面,然後再次優雅地潛回深海,繼續未完的探索。