[程式原理] 非同步 async/await 常見誤用,你是不是也踩過這些雷?
前言
欸,各位工程師朋友好啊!今天 Hikari 我要來跟大家聊一個,在現代程式設計中幾乎無所不在,但又常常讓人搞得頭昏腦脹的議題:「非同步程式設計」!特別是當我們有了 async/await 這種語法糖之後,寫非同步程式碼看起來好像變簡單了,程式碼也變得跟同步執行一樣好讀。但俗話說得好,水能載舟亦能覆舟,async/await 在簡化了語法的同時,其實也埋下了不少「地雷區」,讓很多開發者在不知不覺中踩到,導致程式碼不如預期、效能低落,甚至出現難以追查的 bug。
你是不是也曾經寫過 async/await 的程式碼,卻發現:
- 為什麼這個函式明明有
await,結果卻馬上執行下一行? - 奇怪,我發了三個網路請求,怎麼等了這麼久?不是說非同步可以同時做嗎?
- 啊,明明加了
try/catch,怎麼錯誤還是噴出來了?
別擔心,你不是一個人!這些都是 async/await 非常常見的誤用情境。今天,我們就來好好拆解這些「雷區」,看看背後的原理,以及怎麼正確地使用 async/await,讓你的非同步程式碼既優雅又高效!
快速回顧:async/await 到底是什麼?
在深入探討誤用之前,我們還是快速複習一下 async/await 的核心概念吧!
想像一下,你是一個餐廳服務生(你的程式碼),老闆(系統)給你一張點餐單(一系列任務)。有些菜(任務)會很快做好,有些菜(任務)則需要等待廚師(外部資源,例如資料庫、API、檔案讀寫)處理很久。
async關鍵字:當你把一個函式標記為async時,你就是在告訴老闆:「這個函式裡面可能會做一些需要等待很久的『非同步』事情哦!」所以,當你呼叫這個async函式時,它不會直接回傳最終結果,而是會回傳一個「承諾」(Promise,或是 C# 的 Task)給你。這個承諾代表著「這個任務未來會給你一個結果」的保證。程式碼執行到這裡並不會卡住,它會繼續往下執行。await關鍵字:當你在async函式裡面,看到某個需要等待的任務(通常是回傳 Promise/Task 的函式)前面加上await時,你就是在告訴自己:「好,這個菜還沒好,我先在這裡等一下,直到它做好為止。這段時間我可以去服務其他客人,但等這個菜好了,我再回來處理它後續的事情。」當await等待的任務完成後,程式碼會從await的地方恢復執行,並且拿到那個任務的最終結果。
簡單來說:async 讓函式變成非同步的,它會回傳一個 Promise/Task;await 則是在 async 函式內部,讓你「暫停」當前函式的執行,等待一個 Promise/Task 完成,然後「恢復」執行並拿到結果。
下面我們用 JavaScript 的例子來示範,但這些概念在 C#、Python 等支援 async/await 的語言中都是相通的。
1 | async function fetchUserData(userId) { |
這段程式碼會依序執行 fetchUserData(1) 和 fetchUserData(2),總共會花費大約 4 秒。現在,我們來看看常見的誤用情況,以及怎麼改善它!
常見誤用 1:呼叫 async 函式卻忘記 await
這是最最最常見的誤用之一!當你呼叫一個 async 函式時,它會馬上回傳一個 Promise。如果你沒有 await 它,程式碼就會「繼續往下跑」,而不會等待那個 Promise 完成。這就像你點了一道菜,服務生給你一個號碼牌,你卻直接走掉,期望菜能自己送到你手上,甚至你不知道菜到底做好了沒。
問題情境:
假設我們有一個 logAnalytics 的非同步函式,用來紀錄使用者行為,這個動作不影響主要流程,所以我們呼叫它,但「忘了」等待它完成。
1 | async function logAnalytics(event) { |
你會發現 事件 order_placed 已記錄。 這行可能會比 訂單處理完成... 還晚出現,甚至如果主程式提早結束,這行可能根本就不會印出來。這會導致資料不一致或遺漏。
解法:
只要你的邏輯需要等待非同步操作完成,就一定要加上 await!
1 | async function processOrder() { |
如果 logAnalytics 真的不需要等待,那你可以考慮它是不是一個「fire-and-forget」的任務,或者使用 .catch() 來處理可能的錯誤,避免未被捕捉的 Promise Rejection。
1 | // 如果真的不需要等待,至少處理一下潛在錯誤 |
常見誤用 2:錯誤的並行處理(全部串行)
async/await 的一個大優勢就是可以讓多個「不相關」的非同步任務並行執行,然後再統一等待它們的結果。但很多開發者在使用 await 時,不小心把所有任務都寫成了串行執行,浪費了並行的優勢。
問題情境:
假設我們需要從兩個不同的 API 抓取資料,這兩個請求之間沒有依賴關係。
1 | async function fetchProductDetails(productId) { |
這裡的 fetchProductDetails 和 fetchRecommendedProducts 之間沒有依賴,但因為我們用了兩個 await,導致它們會依序執行。總共會花費 3.5 秒。
解法:使用 Promise.all() 來並行執行
當多個非同步任務之間沒有依賴關係時,可以同時啟動它們,然後用 Promise.all() 等待所有任務都完成。這樣總耗時會取決於最慢的那個任務。
1 | async function getProductPageDataConcurrent() { |
這樣一來,總時間就縮短到了最長那個任務的時間(2 秒),大大提升了效率!在 C# 中,對應的函式是 Task.WhenAll()。
常見誤用 3:在迴圈中 await (導致效率低下)
這跟上一個問題很類似,但更常發生在處理列表資料時。很多人習慣在 for 迴圈或 forEach 迴圈中直接使用 await,這會讓迴圈內的每次迭代都變成串行等待,效率極低。
問題情境:
假設我們有一個使用者 ID 列表,需要為每個使用者發送通知。
1 | async function sendNotification(userId) { |
如果 userIds 有 100 個使用者,這樣做會耗費 100 秒!這顯然不是我們想要的。
解法:使用 map 搭配 Promise.all() 並行處理
當迴圈內的任務之間沒有依賴時,同樣可以使用 Promise.all() 來並行處理。通常我們會先用 map 把每個任務變成一個 Promise,然後把這些 Promise 組成一個陣列傳給 Promise.all()。
1 | async function notifyAllUsersConcurrent(userIds) { |
這樣,無論 userIds 有多少個,總時間都約等於最慢的那個 sendNotification 的時間(這裡就是 1 秒)。
注意事項:
雖然並行處理能提高效率,但也要注意資源限制。如果你要同時發送 10000 個網路請求,那可能會因為連接數限制、記憶體壓力等導致問題。這時候可能需要考慮「批次處理」或「限制並行數量」(例如使用 p-limit 這類的函式庫)。
常見誤用 4:忽略錯誤處理(把 try/catch 寫錯地方)
錯誤處理在非同步程式碼中是個大魔王!很多人會把 try/catch 寫錯地方,導致非同步操作拋出的錯誤沒有被捕捉到,最後整個程式崩潰。
問題情境:
假設 fetchUserData 有可能失敗,但 try/catch 沒有正確包住 await。
1 | async function riskyFetchUserData(userId) { |
在第一個 try 區塊中,riskyFetchUserData(99) 雖然會執行並拋出錯誤,但因為 await 不在 try 區塊內,try/catch 並沒有等待那個 Promise 失敗,所以它根本抓不到這個錯誤。錯誤會直接變成一個未處理的 Promise Rejection,導致程式崩潰或錯誤被丟到全域去。
解法:將 await 包在 try 區塊內
正確的錯誤處理方式,是讓 await 語句本身在 try 區塊內執行。這樣,當 await 等待的 Promise 被 rejected 時,try/catch 就能立即捕捉到它。
1 | async function getUserDataWithCorrectErrorHandling() { |
確保你的 await 都在 try 區塊裡面,才能有效地捕捉到非同步操作可能拋出的錯誤!
小結
async/await 確實是現代程式設計中一個非常棒的工具,它讓我們能用接近同步的寫法來處理複雜的非同步流程,大大提升了程式碼的可讀性和可維護性。但正如我們今天聊的,如果不了解它背後的運作原理和一些常見的「眉角」,還是很容易踩到雷,導致程式碼行為不如預期,甚至影響效能和穩定性。
下次你在寫 async/await 的程式碼時,不妨在心中問自己幾個問題:
- 這個
async函式回傳的 Promise,我有確實await嗎? - 這幾個非同步任務之間有依賴關係嗎?如果沒有,我是否可以考慮用
Promise.all()讓它們並行處理來提升效率? - 我在迴圈裡面使用了
await嗎?這樣做是故意的串行還是不小心的效能瓶頸? - 我的
try/catch有正確地包住await語句,確保能捕捉到非同步錯誤嗎?
多花一點心思去思考這些問題,你就能寫出更強健、更高效的非同步程式碼。希望今天的分享,能幫助大家在 async/await 的世界裡,走得更穩、更遠!下次我們再聊其他工程筆記!掰掰!
如果您喜歡我寫的文章,幫我按個5下讚吧!感謝您的鼓勵和支持!



![[AI] 提示詞注入(Prompt Injection)與防禦:讓你的 LLM 應用不再被綁架!](/img/covers/prompt-injection-defense.jpg)
![[AI] LLM 的 Function Calling / Tool Use 怎麼運作?白話拆解原理與實戰眉角](/img/covers/llm-function-calling-tool-use-how-it-works.jpg)