Promise 中如何使用 try/catch 處理非同步錯誤教學

  • 了解 JavaScript 引擎、呼叫堆疊和任務佇列是預測非同步程式碼中錯誤處理程序如何以及何時運行的關鍵。
  • Promise 引入了「不可見的 try/catch」機制,可以捕獲異常並允許鍊式呼叫。 then y catch 以集中且易於理解的方式管理錯誤。
  • 文法 async/await 使用程式碼區塊簡化錯誤處理。 try/catch 經典方法更傾向於順序編寫非同步程式碼。
  • 全球事件,例如 unhandledrejection 正確的組合模式有助於避免未處理的拒絕,並設計強大的錯誤日誌記錄和通知策略。

使用 try/catch 處理非同步錯誤

在 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:狀態、鍊式呼叫與錯誤處理

使用 try/catch 處理非同步錯誤

一 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() 但不要返回它。如果這樣做,鏈就會「分裂」成兩條線:一邊是原來的承諾繼續執行,另一邊是新的內部承諾,但這不會影響主序列。

Linux 上的 Jotta-cli
相關文章:
如何在 Linux 中使用 Crontab 安排自動任務

黃金法則是 “如果在處理程序中創建了 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 處理非同步錯誤
相關文章:
Windows 上最佳的 IDE

結合承諾, try/catch 以及諸如此類的全球事件 unhandledrejection可以建立這樣的應用程式:非同步錯誤得到控制,用戶收到清晰的訊息,開發團隊擁有必要的資訊來調試和改進程式碼,而不會出現應用程式在沒有向用戶解釋的情況下「崩潰」的情況。 分享這份程式設計指南,幫助其他使用者使用 Try/catch 解決非同步錯誤。


在 Google 中將其新增為首選來源