在 JavaScript 中正確處理非同步錯誤,對於打造專業應用和那些動不動就崩潰的應用程式來說,至關重要。當你開始使用 Promise 和 async/await 時,常見的做法是使用 try/catch 區塊“因為這是必要的”,但如果沒有清晰的策略,很容易導致出現靜默錯誤、對用戶毫無幫助的信息,以及控制台上到處都是“未處理的 Promise 拒絕”的警告。
在本文中,我們將冷靜地結合實際例子,以了解如何建立一個 Promise 中使用 try/catch 處理非同步錯誤的完整教學課程JavaScript 引擎底層的工作原理是什麼?任務隊列和微任務扮演什麼角色?以及如何正確地將它們結合? .then(), .catch() y async/await 避免瀏覽器和 Node.js 中出現任何未解決的錯誤。
JavaScript 的內部運作原理是什麼?為什麼它會影響錯誤處理?
在深入探討如何使用 Promise 進行錯誤處理之前,先了解一些JavaScript 引擎、呼叫堆疊和執行階段環境的知識會很有幫助。這可以解釋為什麼某些錯誤會「稍後」出現,或者為什麼 Promise 的行為與傳統的回呼函數有所不同。
執行環境、上下文和呼叫堆疊
JavaScript 在單執行緒上運行(至少在概念上是這樣),這意味著引擎一次只能做一件事。 呼叫棧上的操作一次只做一件事。為了避免在等待網絡、磁碟或 DOM 回應時阻塞整個網絡,瀏覽器會暴露 Web APIs 允許非同步操作,並在之後透過回調、承諾或其他方式傳回結果。 async/await.
所有 JavaScript 程式碼都儲存在某個地方。 執行上下文我們有全域上下文(腳本或頁面載入時建立的第一個上下文),每次呼叫函數時,都會建立一個新的函數上下文,其中包含其變數及其 this 以及指向外部上下文的連結。這些上下文堆疊在一起。 呼叫堆疊 (呼叫堆疊)遵循後進先出(LIFO)策略:最後進入的函數最先被呼叫。
當你呼叫類似這樣的函數時 first()它被放置在堆疊上;如果在堆疊內呼叫它 second() 以及這 third()你會看到堆疊是如何不斷地添加和刪除上下文的,以及每個上下文。 控制台訊息反映了堆疊的進入和退出順序。諸如瀏覽器調試器、斷點或之類的工具 console.trace() 它們允許您即時檢查堆疊。
任務佇列和微任務:為什麼 Promise 會被「稍後」執行
JavaScript 引擎也支援多種技術。 任務佇列這裡最相關的就是宏任務佇列和微任務佇列。巨集任務佇列包含以下內容: setTimeout, setInterval 或 DOM 事件。微任務隊列包括(但不限於): Promise 的回調 以及 DOM 突變觀察器。
事件循環的工作原理如下:先執行同步程式碼,然後處理巨集任務,接著… 微任務隊列(所有待處理任務)只有在這種情況下,下一個巨集任務才會開始。這就是為什麼 then() 或 catch() 與承諾關聯的事件會在觸發之前發生。 setTimeout(..., 0)雖然兩者看起來都很「立竿見影」。
這一細節對於理解何時發生至關重要。 Promise 錯誤處理程序 為什麼在某個內部發生的錯誤會造成影響? .then() 它不會立即導致腳本崩潰,而是在事件週期的稍後階段出現。
回調、回調地獄以及向 Promise 的過渡
在 JavaScript 中,非同步操作歷來都是透過回呼函數處理的。一個函數會接收另一個函數作為參數,並在任務完成後執行該回呼函數:例如向伺服器發送請求、計時器、使用 File API 讀取檔案、對 IndexedDB 進行操作等等。
想像一個函數 fetchData(callback) 它內部調用 setTimeout 模擬網路請求,幾秒鐘後, 呼叫回調函數並傳入數據同時,程式繼續執行,顯示諸如「正在獲取資料…」之類的訊息。這表示回呼函數會在事件到達佇列並被引擎處理後執行。
回調地獄:當回調變得難以管理時
當你開始將相互依賴的操作串連起來時,事情就會變得複雜。例如,你處理一個按鈕點擊事件,然後發出一個請求… XMLHttpRequest (不作任何承諾)你使用 setTimeout 要延遲處理,您需要從文件中讀取一個文件。 <input type="file">然後將結果儲存到 indexedDB這些API均期望 不同的回調.
如果你嘗試用嵌套回調來協調所有事情,最終會得到臭名昭著的“厄運金字塔”或回調地獄:大量的縮進,難以閱讀,幾乎無法維護,而且在處理錯誤時會變成一場噩夢,因為你必須在每一層重複錯誤邏輯。
這種情況是 JavaScript 引入Promise的主要原因之一:一種更具聲明性和可管理性的方式來表示非同步操作及其潛在失敗。
Promise:狀態、鍊式呼叫與錯誤處理

一 Promise 它是代表以下對象的 非同步操作的未來結果它可以成功完成,也可以失敗。在內部,它有三種狀態:待處理、已完成(已解決)或拒絕。建構函數 Promise 它接收一個「執行器」函數,該函數又接收兩個函數: resolve y reject.
例如,您可以建立一個 Promise,該 Promise 在經過一段隨機時間後調用 resolve() o reject() 根據某種條件然後,使用 then()你需要定義承諾兌現後該做什麼,以及… catch() 如果請求被拒絕該怎麼辦?這種模式允許你立即返回一個物件(Promise),該物件將作為未來結果的「容器」。
消費承諾:然後,捕捉與連鎖
現代函數通常直接傳回一個 Promise。你無需分別傳遞成功和失敗回調,而是直接接收一個 Promise。 在承諾中,你可以將回呼函數「附加」到該承諾中。 then() y catch()例如,一個函數 crearArchivoAudioAsync() 它可以返回一個 Promise,而不是等待舊式的回調。
真正的力量來自於 一連串的承諾。 方法 then() 它總是返回一個新的 Promise。這意味著你可以這樣寫:
hazAlgo() 接下來 .then(hazAquello)這又會回到另一個 Promise,你可以將它連結到該 Promise 上。 .then(hazOtraCosa)以此類推。每一步都代表前一步的完成,如果其中任何一步… 回調函數會傳回另一個 Promise。此鏈會自動調整,等待新的承諾得到解決。
透過這個模式,先前由嵌套回調構成的金字塔結構變成了 非同步操作的扁平鏈更易於閱讀。此外,錯誤可以透過單一元件集中處理。 .catch() 在鏈條的末端。
承諾中的“隱形嘗試/捕獲”
這些承諾的關鍵點之一是內部執行器和處理程序(then, catch)包含一個 “嘗試/捕捉隱形”。例如,使用 JavaScript 中的 arrow 和 this 函數在 Promise 內部或處理程序的程式碼中拋出的任何異常都會自動導致 Promise 被拒絕。
因此,程式碼如下:
new Promise((resolve, reject) => { throw new Error("oops") })
在功能上等同於顯式調用 reject(new Error("oops"))這同樣適用於回呼函數。 .then()如果你這樣做 throw 它內部不會導致整個腳本崩潰;相反,產生的 Promise 會進入拒絕狀態,控制權會跳到鏈中的下一個錯誤處理程序。
錯誤處理:捕獲、傳播和重新拋出
方法 .catch() 它會捕捉鏈中前一個 Promise 或上一個 Promise 中發生的任何拒絕,前提是該錯誤尚未被處理。您可以放置一個 .catch() 在一長串的末尾 .then() 集中處理錯誤,就像您擁有一個單一的 try..catch 封裝多個程式碼區塊。
內 .catch() 您可以分析錯誤類型(例如,區分 a URIError 或其他程式錯誤),如果你知道該如何處理,就去處理;如果你處理不了, 重新發布錯誤 throw這樣做,控制權將跳到下一個處理程序。 .catch() 鏈條,從而可以設計分層恢復策略。
你也可以製作一個 .catch() 成功「關閉」錯誤(不重新拋出),並傳回恢復值。在這種情況下,鍊式呼叫將繼續其正常流程,並執行下一個操作。 then 它運行起來就好像一切都很順利,得到了那個“替代”值。
請記住 .then() 它可以接收兩個參數:第一個參數表示成功,第二個參數表示失敗。第二個參數的作用是… 該特定階段的本機錯誤處理程序然而,在實踐中,更常見的是使用 .catch() 分開處理,這樣可以更清楚地展示錯誤流程。
未處理的錯誤會發生什麼?
如果承諾被拒絕,且沒有其他解釋。 錯誤處理程序(.catch() 或者第二個論點 .then()) 在鍊式呼叫中,錯誤被“卡住”,引擎將其視為未處理的拒絕。這與發生在非程式碼區塊內的同步異常的情況非常相似。 try..catch腳本“崩潰”,您會在控制台中看到該訊息。
在現代瀏覽器中,您可以使用事件全域攔截這些錯誤。 unhandledrejection本次活動獲得 PromiseRejectionEvent 擁有房產 promise (未能兌現的承諾)和 reason (被拒的原因)。透過這一點,你可以… 在伺服器上記錄錯誤,並顯示用戶友好的消息 或者,至少要防止故障不被忽視。
還有這項活動 rejectionhandled當一個被拒絕的 Promise 物件之後收到錯誤處理程序時,就會觸發此事件。這兩個事件都允許我們制定一套全面的策略來觀察和處理 Promise 錯誤。
在Node.js中,環境會發出類似的事件(unhandledRejection這些事件預設會記錄到控制台。您可以掛鉤到這些事件以進行操作。 集中式錯誤日誌記錄 或防止標準輸出被不受控制的痕跡填充。
使用 Promise 處理錯誤的實用模式
為了確保 Promise 具有強大的錯誤處理能力,遵循某些組合模式並避免破壞控制鏈的典型錯誤非常重要。
永遠信守承諾。
一個非常常見的錯誤是在承諾中創建承諾 .then() 但不要返回它。如果這樣做,鏈就會「分裂」成兩條線:一邊是原來的承諾繼續執行,另一邊是新的內部承諾,但這不會影響主序列。
黃金法則是 “如果在處理程序中創建了 Promise,請返回它。”. 就這樣 then 上級部門會等待內部承諾結束(得到解決或被否定),而錯誤將沿著單一路徑發展。 .catch() 統一處理,而不是分散在不同的處理器中。
最好以一個結尾來結束這些鏈條。 .catch() 最後這樣可以確保任何先前未處理的錯誤最終都會被捕獲,防止它們變成全局性的未管理拒絕。
避免不必要的嵌套,並捕獲
另一種經典的反模式是嵌套。 .then() 嵌套過深會導致兩個元素相互嵌套,而不是相互連結。 錯誤範圍令人困惑 這通常與先前的錯誤(未返回內部承諾)同時發生。
只有當你想要…時,嵌套才有意義。 處理程式 catch 內部僅涵蓋鏈條的特定部分。例如,您可以嵌套一個包含一系列「可選」步驟的程式碼區塊,並使其包含一個 catch 專門用於僅捕獲該部分中的錯誤,而 catch 外部區域用於處理流程其餘部分的關鍵錯誤。
在這種模式下,可選程式碼區塊中的錯誤會在下列位置處理: catch 內部執行,一旦問題解決,後續關鍵任務即可繼續執行。如果在可選程式碼區塊之前的關鍵部分發生錯誤,則該故障完全由內部執行承擔。 catch 鏈條末端.
將舊版回呼 API 封裝到 Promise 中
許多較舊的API,例如 setTimeout() 一些舊版庫仍然需要“舊式”回調函數,並且不返回 Promise。如果在基於 Promise 的程式碼中直接呼叫這些函數,就會遺失 Promise 的回傳值。 能夠統一地捕捉錯誤.
解決方案是將這些 API 封裝在一個建構函式中。 Promise例如,您可以建立 delay(ms) 經過一段時間後,內部返回已履行的承諾 setTimeout從那時起,你總是使用 傳回 Promise 的包裝函數永遠不要使用舊的 API,這樣你的所有非同步程式碼才能「與 Promise 相容」。
加 new Promise()您可以使用快捷方式,例如 Promise.resolve() y Promise.reject() 快速建立已解決或已拒絕的承諾,可用於組合或測試。
並行或依序編寫承諾
要並行執行多個非同步操作並等待它們全部完成,您需要: Promise.all()此方法接收一個 Promise 陣列並傳回一個 Promise。 當所有人都成功時,承諾即告兌現。或者,只要其中一項失敗,整個過程就會被拒絕。
如果你有一個數組,並且想要為每個元素創建一個 Promise,你可以使用 .map() 將其轉化為一系列承諾並傳遞給 Promise.all()如果你不在乎解題順序,只要所有題目都能完成,那麼這個模式就是理想的。
對於數組的順序組合,您可以使用 reduce() 建立一個 等同於一系列承諾 Promise.resolve().then(func1).then(func2)...甚至可以將這種模式封裝在一個組合函數中(composeAsync) 接收多個函數(同步或非同步),並傳回一個新函數,該函數將它們全部連結在一起,並賦予一個初始值。
在現代 ECMAScript 中,許多此類組合在使用上會變得更加自然。 async/await正如我們接下來將看到的。
Async/Await:語法更簡潔,錯誤模型相同
關鍵字 async y await 它們提供了一種更易讀的方式來處理 Promise。 標記為函數 async 它總是返回一個 Promise 對象,即使從內部來看,你似乎返回的是一個普通值。 await 它允許你「暫停」該函數的執行,直到 Promise 被解決或拒絕。
例如,一個函數 async function getUser() 你可以打電話 await fetch(...) 然後到 await response.json()最後傳回結果物件。在外部,無論誰調用 getUser() 收到一個承諾,承諾將根據該數據得到解決。
在非同步函數中使用 try/catch 進行錯誤處理
巨大的好處 async/await 您可以使用 bloques try/catch “終身” 處理異步錯誤。如果預期 Promise 存在,則需要處理非同步錯誤。 await 請求被拒絕,函數內部拋出了一個異常。 async你可以用以下方式捕捉它 catch.
這使得錯誤處理與同步程式碼非常相似:你將錯誤處理分組 await 評論家們 try你分析 例外情況 catch你需要記錄技術細節以便調試,向用戶顯示易於理解的訊息,並在適當的時候重新拋出錯誤,以便更高層可以決定如何處理。
你也可以設定一個方塊 finally 無論是否發生錯誤,它都會一直運行,非常適合釋放資源、重置負載狀態或關閉臨時連線。
呼叫順序執行和呼叫之間的依賴關係
當一個操作依賴另一個操作的結果時, await 它極大地簡化了工作流程。您可以先發起一個初始呼叫來收集使用者訊息,從中提取他們的儲存庫 URL,然後發起第二個請求。 await, 確保第二個程式僅在第一個程式成功完成後才執行。.
這種風格可以防止擴散 .then() 嵌套式的實作使得程式碼看起來幾乎是同步的,儘管底層仍然使用 Promise 和基於微任務佇列的相同執行模型。
模組中的頂級 await
現代ES模組允許使用 await 直接進入更高層級無需將程式碼包裝在函數中 async這適用於載入初始配置、取得渲染所需資料或發出請求等情況。 fetch 其結果立即用於填充模組的全域結構。
透過這種方法,你可以寫出類似這樣的內容: const colors = await fetch(...) 在檔案頂部,該 Promise 會被初始化,並且只有當該 Promise 被解析後,模組才會被視為已完全執行。錯誤處理機制保持不變:如果該 Promise 被拒絕且未被處理,則會作為模組錯誤傳播。
Promise 與全域錯誤處理之間的關係
Promise 解決了基於回調模型的一個根本問題:捕捉所有錯誤(包括程式異常),並將其儲存在一個便於非同步任務功能組合的結構中。
當一個 Promise 被拒絕且無人處理時,執行時會發出對應的全域事件(unhandledrejection 在瀏覽器中, unhandledRejection (在 Node.js 中)。這對於創建最後一層防禦非常有用:記錄錯誤、通知監控系統、向使用者顯示一般訊息,或至少防止這些錯誤不受控制地湧入控制台。
撥電至 event.preventDefault() 在這些處理程序中,您可以 禁用環境的預設行為 (例如,將追蹤資訊列印到控制台),前提是您已決定以其他方式自行處理。無論如何,在忽略任何被拒絕的 Promise 之前,建議您仔細檢查它們是否是真正的程式錯誤。
穩健的非同步錯誤處理策略的最佳實踐
為了更全面地了解情況,有必要總結一些有助於實現以下目標的指導原則: 強大的錯誤處理系統 在非同步程式碼中使用 Promise 和 async/await:
- 地點
.catch()就在你知道該如何應對的地方並非必須捕獲所有錯誤;有些錯誤是無法恢復的,最好讓錯誤升級,只進行全域監控。 - 利用第二個論點
.then()僅當需要局部治療時才需要。在大多數情況下,.catch()單一字串更易讀,也不容易出錯。 - 不要用過多的技術細節讓使用者感到不知所措。它在介面上顯示清晰易懂、人性化的訊息,並將技術細節(堆疊追蹤、有效載荷等)保留給內部日誌記錄或監控系統。
- 集中式錯誤日誌記錄無論是使用全域非託管承諾事件、中間件、HTTP攔截器還是共用實用程序,都要盡量減少決定要記錄什麼以及如何記錄的點。
- 永遠信守你所相信的承諾,並掙脫束縛。
catch這大大減少了未捕獲的錯誤,並使您的控制流程可預測。 - 表演
async, 不要忘記await疏忽可能導致被拒絕的承諾不受約束。try/catch逃到你意想不到的地方去處理這件事。
結合承諾, try/catch 以及諸如此類的全球事件 unhandledrejection可以建立這樣的應用程式:非同步錯誤得到控制,用戶收到清晰的訊息,開發團隊擁有必要的資訊來調試和改進程式碼,而不會出現應用程式在沒有向用戶解釋的情況下「崩潰」的情況。 分享這份程式設計指南,幫助其他使用者使用 Try/catch 解決非同步錯誤。