
她在深夜寫了一段訊息,對著那個熟悉的對話框,輕輕按下「傳送」
網路似乎是卡住了,畫面顯示「正在傳送…」,她關上了手機,心想:「應該已經送出了吧。」
隔天,她等不到回應。那人說沒收到。
她打開訊息記錄,那串話仍顯示為「未送達」,就像訊息從未存在過
一個沒有被接收的訊息,不會自動變成世界的理解;一個沒有被等待的結果,也無法保證會如你所想地完成

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

這時,許多開發者會選擇以下方式強行「同步化」等待
.Wait()
.Result
GetAwaiter().GetResult()
它們都會強迫等待 Task 結束,但錯誤處理機制卻有天壤之別,而這個差異,正是許多初學者第一次遇到的陷阱。
以下,我們將模仿實驗,實際觀察其錯誤行為與影響,像是 .Wait() 和 .Result 不是把錯誤丟給你,而是「包一層再丟」,讓你更難看清真相

實驗出處
你有一個 async 方法
1 2 3 4 5 6 7 8 9 10 11
| static async Task<string> SumulateDatabaseConnectionAsync() { Thread.Sleep(2000); if (DateTime.Now > new DateTime(2020, 12, 12)) { throw new InvalidDataException("db 連壞掉啦!!"); } return "db 連到啦"; }
|
你身處同步世界
我們用同一個會「一定丟錯」的 async 方法,換三種同步等待方式,看 catch 到的是什麼鬼東西
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
| 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(); }
|

.Wait() / .Result
.Wait() 和 .Result 是 「同步阻塞 async Task」 的做法,在 .NET 設計上一個 Task 可能同時有多個例外,所以它一定用 AggregateException 當外殼,它不是把錯誤丟給你,而是先幫你包一個你沒要的殼再丟。.Wait() / .Result 會把真正的例外包進 AggregateException,當我們 catch (Exception ex) 時 ex.Message 是 假的,真正錯誤藏在 InnerException

GetAwaiter().GetResult()
它模擬的就是 await 的行為,只是少了語法糖,async 裡丟什麼,你就接到什麼,而 stack trace 也比較乾淨

Fire-and-Forget(async lambda)
你以為你在等,其實你只是假裝沒看到錯誤。TestSip 接的是 Action,async lambda 會變成 async void,例外不會回到 TestSip、也 catch 不到,直接交給 runtime
