Async

她在深夜寫好了一段訊息,對著那個熟悉的對話框,輕聲按下「傳送」。

網路卡住了,畫面上顯示「正在傳送…」,她盯著那行字等了一秒、兩秒,終究關上了手機,心想:「應該已經送出了吧。」

隔天,她等不到回應。那人說什麼都沒收到。

她反覆打開訊息記錄,那串話仍顯示為「未送達」,像是從未存在過——但她明明說過了。

一個沒有被接收的訊息,不會自動變成世界的理解;一個沒有被等待的結果,也無法保證會如你所想地完成。

🎵 非同步的錯誤處理:等待與錯誤的交錯人生

在實務開發中,我們經常面臨一種情境:必須等待某個非同步任務完成,才能繼續執行接下來的邏輯。但如果我們當下的環境是同步方法(例如 Main() 或某些事件處理函式),該怎麼辦?

這時,許多開發者會選擇以下方式強行「同步化」等待:

  • .Wait()
  • .Result
  • GetAwaiter().GetResult()

它們都會強迫等待 Task 結束,但錯誤處理機制卻有天壤之別,而這個差異,正是許多初學者第一次遇到的陷阱。
以下,我們將模仿實驗,實際觀察其錯誤行為與影響。

實驗出處

🎵 實驗

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

static void Main()
{
TestSip("Wait", () => {
SumulateDatabaseConnectionAsync().Wait();
});

TestSip("Result", () =>
{
var result = SumulateDatabaseConnectionAsync().Result;
});

TestSip("使用 GetAwaiter().GetResult()", () =>
{
var result = SumulateDatabaseConnectionAsync().GetAwaiter().GetResult();
});


TestSip("Fire And Forget", async () => {
await SumulateDatabaseConnectionAsync();
});

WriteColorLine("主程序結束!", ConsoleColor.Cyan);
}

static void TestSip(string testName, Action callback)
{
WriteColorLine("=========================================", ConsoleColor.Yellow);
WriteColorLine($"測試 : {testName}", ConsoleColor.Green);
WriteColorLine($"開始時間 : {DateTime.Now:HH:mm:ss}", ConsoleColor.Gray);

try
{
callback();
WriteColorLine($"結束時間 : {DateTime.Now:HH:mm:ss}", ConsoleColor.Gray);
WriteColorLine("操作成功完成", ConsoleColor.Green);
}
catch (Exception ex)
{
WriteColorLine($"結束時間 : {DateTime.Now:HH:mm:ss}", ConsoleColor.Red);
WriteColorLine($"錯誤 : {ex.Message}", ConsoleColor.Red);
if (ex.InnerException != null)
{
WriteColorLine($"內部錯誤 : {ex.InnerException.Message}", ConsoleColor.Red);
}
}
}

static void WriteColorLine(string message, ConsoleColor colorName)
{
Console.ForegroundColor = colorName;
Console.WriteLine(message);
Console.ResetColor();
}


static async Task<string> SumulateDatabaseConnectionAsync()
{
Thread.Sleep(2000);
if (DateTime.Now > new DateTime(2020, 12, 12))
{
throw new InvalidDataException("db 連壞掉啦!!");
}

return "db 連到啦";
}

🎵 結果分析

結果分析

.Wait() : AggregateException

內部實際錯誤會被包裝在 .InnerException 中,需特別處理。

.Result : AggregateException

與 .Wait() 相同,異常會被封裝,開發者無法直接取得原始錯誤訊息。

.GetAwaiter().GetResult() : 原始例外直接拋出

直接拋出 InvalidDataException,不會包裝,是較利於除錯的方式。

Fire-and-Forget(async lambda) : 錯誤無法被捕捉

因為 TestSip 接收的是 Action 而非 Func,async lambda 被當作 fire-and-forget,未 await 即結束,因此異常直接被拋棄,極可能造成背景錯誤或應用程式崩潰。

總得來說,.Wait() 和 .Result 看似方便,卻把原本清楚的例外訊息包裹成 AggregateException,讓你在錯誤發生時需要額外拆解 .InnerException 才能看見真正的問題。而 GetAwaiter().GetResult() 雖然仍是同步化操作,但至少保留了例外的原貌,比較有助於追蹤與除錯。

至於 Fire-and-Forget,看似「寫起來很簡潔」,但如果你沒有 await 它,就像是把一個快遞丟進宇宙,既沒有追蹤編號,也無法確認它是否送達──這種錯誤最危險,因為你連錯在哪裡都無從得知。

🎵 結語 : 錯誤從未消失,只是你沒有等待它

她說的話其實早已傳出,只是訊號還在半途,還沒抵達。

就像程式裡的那些非同步,它們正在途中、正在處理、正在為你奔走,但你太快關上了門,錯誤悄悄地留在背景,從未被發現,也從未被理解。.Wait()、.Result(),這些強行催促的語句,像極了我們在人生裡不願等待他人完成話語、不肯聽完事情全貌時的急切。

有些事,需要時間完成,有些錯,需要完整等待,才會被我們看見、擁抱、甚至修正。下一次,當你面對一段非同步時,記得別急著轉身。有些結果,值得等待;有些錯誤,只有等完了,才會被你溫柔地接住。