JavaScript錯誤處理權威指南
本文將分三部分分析 JavaScript 中的錯誤,首先我們將瞭解錯誤的一般情況,之後,我們將關注後端(Node.js + Express.js),最後,我們將重點看下如何處理 React.js 中的錯誤。選擇這些框架,是因為它們是目前最流行的,但是,你應該也能夠將這些新發現應用到其他框架中吧!
繼上一篇文章 (https://link.medium.com/MO32x55aNR) 之後,我想談談錯誤。錯誤很好——我相信你以前聽過這個說法。乍一看,我們害怕錯誤,因為錯誤往往會涉及到在公共場合受到傷害或羞辱。通過犯錯誤,我們實際上學會了如何不去做某事,以及下次如何做得更好。
顯然,這是關於從現實生活的錯誤中學習。程式設計中的錯誤有點不同。它們為我們提供了很好的特徵來改進我們的程式碼,並告訴使用者什麼地方出了問題(也可能是教他們如何修復它)。
GitHub(https://github.com/gisderdube/graceful-error-handling) 上提供了一個完整的樣例專案。
JavaScript 錯誤和一般處理
throw new Error('something went wrong') 會在 JavaScript 中建立一個錯誤例項,並停止指令碼的執行,除非你對錯誤做了一些處理。當你作為 JavaScript 開發者開啟自己的職業生涯時,你自己很可能不會這樣做,但是,你已經從其他庫(或執行時)那裡看到了,例如,類似“ReferenceError: fs 未定義”這樣的錯誤。
Error 物件
Error 物件有兩個內建屬性供我們使用。第一個是訊息,作為引數傳遞給 Error 建構函式,例如 new Error(“這是錯誤訊息”)。你可以通過 message 屬性訪問訊息:
const myError = new Error(‘請改進程式碼’)
console.log(myError.message) // 請改進程式碼
第二個是錯誤堆疊跟蹤,這個屬性非常重要。你可以通過 stack 屬性訪問它。錯誤堆疊將為你提供歷史記錄(呼叫堆疊),從中可以檢視哪個檔案導致了錯誤。堆疊的上部也包括訊息,然後是實際的堆疊,從距離錯誤發生最近的點開始,一直到最外層“需要為錯誤負責”的檔案:
Error: 請改進程式碼 at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79) at Module._compile (internal/modules/cjs/loader.js:689:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) at Module.load (internal/modules/cjs/loader.js:599:32) at tryModuleLoad (internal/modules/cjs/loader.js:538:12) at Function.Module._load (internal/modules/cjs/loader.js:530:3) at Function.Module.runMain (internal/modules/cjs/loader.js:742:12) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)
丟擲和處理錯誤
現在,Error 例項本身不會導致任何結果,例如,new Error('...') 不會做任何事情。當錯誤被丟擲時,就會變得更有趣。然後,如前所述,指令碼將停止執行,除非你在流程中以某種方式對它進行了處理。記住,是手動丟擲錯誤,還是由庫丟擲錯誤,甚至由執行時本身(Node 或瀏覽器),都沒有關係。讓我們看看如何在不同的場景中處理這些錯誤。
try .... catch
這是最簡單但經常被遺忘的錯誤處理方法——多虧 async / await,它的使用現在又多了起來。它可以用來捕獲任何型別的同步錯誤,例如,如果我們不把 console.log(b) 放在一個 try … catch 塊中,指令碼會停止執行。
… finally
有時候,不管是否有錯誤,程式碼都需要執行。你可以使用第三個可選塊 finally。通常,這與在 try…catch 語句後面加一行程式碼是一樣的,但它有時很有用。
非同步性——回撥
非同步性,這是在使用 JavaScript 時必須考慮的一個主題。當你有一個非同步函式,並且該函式內部發生錯誤時,你的指令碼將繼續執行,因此,不會立即出現任何錯誤。當使用回撥函式處理非同步函式時(不推薦),你通常會在回撥函式中收到兩個引數,如下所示:
如果有錯誤,err 引數就等同於那個錯誤。如果沒有,引數將是 undefined 或 null。要麼在 if(err) 塊中返回某項內容,要麼將其他指令封裝在 else 塊中,這一點很重要,否則你可能會得到另一個錯誤,例如,result 可能未定義,而你試圖訪問 result.data,類似這樣的情況。
非同步性——Promises
處理非同步性的更好方法是使用 Promises。在這一點上,除了程式碼可讀性更強之外,我們還改進了錯誤處理。只要有一個 catch 塊,我們就不再需要太關注具體的錯誤捕獲。在連結 Promises 時,catch 塊捕獲會自 Promises 執行或上一個 catch 塊以來的所有錯誤。注意,沒有 catch 塊的 Promises 不會終止指令碼,但會給你一條可讀性較差的訊息,比如:
(node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong
(node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */
因此,務必要在 Promises 中加入 catch 塊。
回到 try … catch
隨著 JavaScript 引入 async / await,我們回到了最初的錯誤處理方法,藉助 try … catch … finally,錯誤處理變得非常簡單。
因為這和我們處理“普通”同步錯誤的方法是一樣的,所以如果需要的話,更容易使用作用域更大的 catch 語句。
伺服器端錯誤的產生和處理
現在,我們已經有了處理錯誤的工具,讓我們看下,我們在實際情況下能用它們做什麼。後端錯誤的產生和處理是應用程式至關重要的組成部分。對於錯誤處理,有不同的方法。我將向你展示一個自定義 Error 建構函式和錯誤程式碼的方法,我們可以輕鬆地傳遞到前端或任何 API 消費者。構建後端的細節並不重要,基本思路不變。
我們將使用 Express.js 作為路由框架。讓我們考慮下最有效的錯誤處理結構。我們希望:
-
一般錯誤處理,如某種回退,基本上只是說:“有錯誤,請再試一次或聯絡我們”。這並不是特別聰明,但至少通知使用者,有地方錯了——而不是無限載入或進行類似地處理。
-
特殊錯誤處理為使用者提供詳細資訊,讓使用者瞭解有什麼問題以及如何解決它,例如,有資訊丟失,資料庫中的條目已經存在等等。
構建一個自定義 Error 建構函式
我們將使用已有的 Error 建構函式並擴充套件它。繼承在 JavaScript 中是一件危險的事情,但根據我的經驗,在這種情況下,它非常有用。我們為什麼需要它?我們仍然希望堆疊跟蹤給我們一個很好的除錯體驗。擴充套件 JavaScript 原生 Error 建構函式可以讓我們方便地獲得堆疊跟蹤。我們唯一要做的是新增程式碼(我們稍後可以通過錯誤程式碼訪問)和要傳遞給前端的狀態(http 狀態程式碼)。
如何處理路由
在完成 Error 的自定義之後,我們需要設定路由結構。正如我指出的那樣,我們想要一個單點真錯誤處理,就是說,對於每一個路由,我們要有相同的錯誤處理行為。在預設情況下,由於路由都是封裝的,所以 Express 並不真正支援那種方式。
為了解決這個問題,我們可以實現一個路由處理程式,並把實際的路由邏輯定義為普通的函式。這樣,如果路由功能(或任何內部函式)丟擲一個錯誤,它將返回到路由處理程式,然後可以傳給前端。當後端發生錯誤時,我們可以用以下格式傳遞一個響應給前端——比如一個 JSON API:
{
error: 'SOME_ERROR_CODE',
description: 'Something bad happened. Please try again or contact support.'
}
準備好不知所措。當我說下面的話時,我的學生總是生我的氣:
如果你咋看之下並不是什麼都懂,那沒問題。只要使用一段時間,你就會發現為什麼要那樣。
順便說一下,這可以稱為自上而下的學習,我非常喜歡。
路由處理程式就是這個樣子:
我希望你能讀下程式碼中的註釋,我認為那比我在這裡解釋更有意義。現在,讓我們看下實際的路由檔案是什麼樣子:
在這些例子中,我沒有做任何有實際要求的事情,我只是假設不同的錯誤場景。例如,GET /city 在第 3 行結束,POST /city 在第 8 號結束等等。這也適用於查詢引數,例如,GET /city?startsWith=R。本質上,你會有一個未處理的錯誤,前端會收到:
{
error: 'GENERIC',
description: 'Something went wrong. Please try again or contact support.'
}
或者你將手動丟擲 CustomError,例如:
throw new CustomError('MY_CODE', 400, 'Error description')
上述程式碼會轉換成:
{
error: 'MY_CODE',
description: 'Error description'
}
既然我們有了這個漂亮的後端設定,我們就不會再把錯誤日誌洩漏到前端,而總是返回有用的資訊,說明出現了什麼問題。
確保你已經在 GitHub(https://github.com/gisderdube/graceful-error-handling) 上看過完整的庫。你可以把它用在任何專案中,並根據自己的需要來修改它!
向用戶顯示錯誤
下一個也是最後一個步驟是管理前端的錯誤。這裡,你要使用第一部分描述的工具處理由前端邏輯產生的錯誤。不過,後端的錯誤也要顯示。首先,讓我們看看如何顯示錯誤。如前所述,我們將使用 React 進行演練。
把錯誤儲存在 React 狀態中
和其他資料一樣,錯誤和錯誤訊息會變化,因此,你想把它們放在元件狀態中。在預設情況下,你想要在載入時重置錯誤,以便使用者第一次看到頁面時,不會看到錯誤。
接下來我們必須澄清的是不同錯誤型別及與其匹配的視覺化表示。就像在後端一樣,有 3 種類型:
-
全域性錯誤,例如,其中一個常見的錯誤是來自後端,或者使用者沒有登入等。
-
來自後端的具體錯誤,例如,使用者向後端傳送登入憑證。後端答覆密碼不匹配。前端無法進行此項驗證,所以這樣的資訊只能來自後端。
-
由前端導致的具體錯誤,例如,電子郵件輸入驗證失敗。
2 和 3 非常類似,雖然源頭不一樣,但如果你願意,就可以在同樣的 state 中處理。我們將從程式碼中看下如何實現。
我將使用 React 的原生 state 實現,但是,你還可以使用類似 MobX 或 Redux 這樣的狀態管理系統。
全域性錯誤
通常,我將把這些錯誤儲存在最外層的有狀態元件中,並渲染一個靜態 UI 元素,這可能是螢幕頂部的一個紅色橫幅、模態或其他什麼東西,設計實現由你決定。
讓我們看下程式碼:
正如你看到的那樣,Application.js 中的狀態存在錯誤。我們也有方法可以重置並改變錯誤的值。我們把值和重置方法傳遞給 GlobalError 元件,在點選'x'時,該元件會顯示錯誤並重置它。讓我們看看 GlobalError 元件:
你可以看到,在第 5 行,如果沒有錯誤,我們就不做任何渲染。這可以防止我們的頁面上出現一個空的紅框。當然,你可以改變這個元件的外觀和行為。例如,你可以將“x”替換為 Timeout,幾秒鐘後重置錯誤狀態。
現在,你已經準備好在任何地方使用全域性錯誤狀態了,只是從 Application.js 把 _setError 向下傳遞,而且,你可以設定全域性錯誤,例如,當一個請求從後端返回了欄位 error: 'GENERIC'。例如:
如果你比較懶,到這裡就可以結束了。即使你有具體的錯誤,你總是可以改變全域性錯誤狀態,並把錯誤提示框顯示在頁面頂部。不過,我將向你展示如何處理和顯示具體的錯誤。為什麼?首先,這是關於錯誤處理的權威指南,所以我不能停在這裡。其次,如果你只是把所有的錯誤都作為全域性錯誤來顯示,那麼體驗人員會瘋掉。
處理具體的請求錯誤
和全域性錯誤類似,我們也有位於其他元件內部的區域性錯誤狀態,過程相同:
有件事要記住,清除錯誤通常有一個不同的觸發器。用' x '刪除錯誤是沒有意義的。關於這一點,在發出新請求時清除錯誤會更有意義。你還可以在使用者進行更改時清除錯誤,例如當修改輸入值時。
源於前端的錯誤
如前所述,這些錯誤可以使用與處理後端具體錯誤相同的方式(狀態)進行處理。這次,我們將使用一個有輸入欄位的示例,只允許使用者在實際提供以下輸入時刪除一個城市:
使用錯誤程式碼實現錯誤國際化
也許你一直想知道為什麼我們有這些錯誤程式碼,例如 GENERIC ,我們只是顯示從後端傳遞過來的錯誤描述。現在,隨著你的應用越來越大,你就會希望征服新的市場,並在某個時候面臨多種語言支援的問題。如果你到了這個時候,你就可以使用前面提到的錯誤程式碼使用使用者的語言來顯示恰當的描述。
我希望你對如何處理錯誤有了一些瞭解。忘掉 console.error(err),它現在已經是過去時了。可以使用它進行除錯,但它不應該出現在最終的產品構建中。為了防止這種情況,我建議你使用日誌庫,我過去一直使用 loglevel,我對它非常滿意。
英文原文
本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q群:943129070,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。