
她在深夜寫好了一段訊息,對著那個熟悉的對話框,輕聲按下「傳送」。
網路卡住了,畫面上顯示「正在傳送…」,她盯著那行字等了一秒、兩秒,終究關上了手機,心想:「應該已經送出了吧。」
隔天,她等不到回應。那人說什麼都沒收到。
她反覆打開訊息記錄,那串話仍顯示為「未送達」,像是從未存在過——但她明明說過了。
一個沒有被接收的訊息,不會自動變成世界的理解;一個沒有被等待的結果,也無法保證會如你所想地完成。
🎵 非同步的錯誤處理:等待與錯誤的交錯人生
在實務開發中,我們經常面臨一種情境:必須等待某個非同步任務完成,才能繼續執行接下來的邏輯。但如果我們當下的環境是同步方法(例如 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(),這些強行催促的語句,像極了我們在人生裡不願等待他人完成話語、不肯聽完事情全貌時的急切。
有些事,需要時間完成,有些錯,需要完整等待,才會被我們看見、擁抱、甚至修正。下一次,當你面對一段非同步時,記得別急著轉身。有些結果,值得等待;有些錯誤,只有等完了,才會被你溫柔地接住。