詳解JS中異常與錯誤處理的正確方法

簡介

首先,這篇文章一定會引起爭議,因為對於錯誤處理從來就沒有真正的標準答案,每個人都會有自己的主觀意見。 我的理解畢竟也是片面,提出的想法主要是基於個人的經驗總結,如果有異議,歡迎交流討論。 為瞭能夠盡量保持客觀,我會將處理思想盡量前置,再圍繞處理思想展開。 這樣大傢就能先思考這個思想是否能夠成為前提,以及在這個思想前提下的最優處理方式。

1 面向錯誤編程

為什麼說面向錯誤編程,這裡就要向大傢發出靈魂一問,你能確保你寫的每行代碼都是正常的嗎? 假設你的代碼天衣無縫,但你能確保你調用別人提供的方法也不會出現問題嗎? 顯然,我們無法保證每行代碼的正確性,也無法保證調用別人的方法一定能成功執行。所以如何處理錯誤就成瞭重中之重。 這時候可能就有人說瞭:“當然有可能會出現問題啊,不然要測試幹嘛?”我們需要知道的是,當這個錯誤暴露出來的時候, 可能就造成瞭無法估量的損失。測試人員在進行測試的時候是不知道我們的代碼是如何寫的,是很有可能測試時候漏瞭一些點的, 而這每個遺留點都會成為一個個地雷,說不定哪天就爆瞭。這也是為什麼很多公司都特別重視單元測試的原因。 關於單元測試我們回頭專門講解,今天主要討論面向錯誤編程。為瞭防止出現極端情況,我們需要先考慮錯誤情況,再去考慮正常。 這種編程思想我們將其稱之為面向錯誤編程。

1.1 墨菲定律

墨菲定律講的是一個事件如果有兩種或兩種以上的方式去做某件事情,而其中一種方式將導致災難,則必定有人會做出這種選擇。 就像一艘船足夠穩固,翻船的概率幾乎為零,但是它還是有翻船的可能性,所以就得備好救生衣。 同樣,即使你的代碼出錯概率為百萬分之一,但是那百萬分之一一旦發生,對使用者來說就是100%,所以必須做好應急策略。

1.2 先判否

在代碼出錯這點上,我們可以假設一個極端情況,即我們寫的每行代碼都有可能出錯。 所以在寫代碼之前我們就需要考慮到可能出現錯誤的情況,在代碼上的體現就是我們可能先寫瞭一堆錯誤的處理,最後才去處理正確的邏輯。 面向錯誤編程的核心技巧就是先判否。

function doSomeThing (params) {
  if (!params.a) return 
  if (params.b !== true) return
  // ...
  // 開始寫正確代碼邏輯
}

錯誤不是異常

我們需要明確,異常是代碼運行時發生的異常信息,如果不處理會導致代碼無法運行。而錯誤是代碼在運行過程中一個不期待的結果。 比如發起一個 http 請求,即使出現網絡出錯等情況導致請求失敗,但是請求即使失敗瞭,也應該是不影響代碼繼續運行的。 對於我們來說一個請求隻有一個結果,要麼成功,要麼失敗,而失敗其實也是一個結果,那麼這就是一個錯誤。 但是在執行 JSON.parse 方法的時候如果解析出錯不做處理,就會因為解析異常而導致代碼運行終止,那麼這就是一個異常。

2. js 內置的錯誤處理

2.1 Error 類

當運行時錯誤產生時,Error 對象會被拋出。Error 對象也可用於用戶自定義的異常的基礎對象。js還封裝瞭一些內置的標準錯誤類型。 如語法錯誤的 SyntaxError,類型錯誤的 TypeError,以及 ReferenceError、RangeError、URIError、AggregateError、AggregateError、InternalError。

2.2 throw

throw 用於拋出一個異常。經常結合 Error 一起使用。

2.3 try catch

try catch 用於捕獲一個可能出現的未知異常。剛剛提到的 JSON.parse 就是一個最好的例子,在解析一個字符串的時候, 你沒法確保字符串就一定是一個 json 字符串,所以必須使用 try catch 包裹異常,不然就會發生代碼運行終止。 而我們很多同學在 try catch 的使用過程中經常會發生濫用現象,比如使用 try catch 包裹請求錯誤:

try {
  const res = await request()
  if (res) {
    //...
  }
} catch(err) {
  // ...
  // 請求錯誤處理
}

我個人非常反對這種使用,為什麼呢,按照剛剛說的,請求結果其實是一個值,即使請求失敗,返回的也是一個值。 而在 try 裡面即使拿到請求成功的值,在後續處理 res 的過程中如果發生語法錯誤,這時也會拋出異常, 那此時你還能確定這個 catch 捕獲到的異常是請求失敗瞭還是處理 res 時候報錯的嗎? 我之前說過一句話,濫用 try catch 和隨地大小便無異。所以希望大傢不要再使用這種方法。

2.4 Promise.catch

js 中還提供瞭內置類 Promise,promise 的存在不僅僅是解決瞭 js 回調地獄的問題,而且按照 promise 的設定,promise 必定會返回一個結果。 這個結果要麼是成功,要麼是失敗。這點其實就和剛剛提到的請求結果非常搭配,比如 axios 請求庫就使用 promise 返回請求結果。所以針對上面的例子我們應該這樣

// request 返回一個 promise
request()
.then(res => {
  // 這裡處理請求成功
  // ...
  // 如果業務代碼可能出錯,再使用 try catch
  try {
    // ...
  } catch (err) {
    // ...
  }
})
.catch()

3. 錯誤處理隻有一次

代碼運行出錯時,我們隻處理一次。如果你能立即確定錯誤的處理方案,你就直接處理掉。 當你處理不瞭這個錯誤的時候,就要把詳細的錯誤結果包裝好。這樣別人在調用的時候就能自己去處理錯誤瞭。

這裡我們繼續使用請求來舉例,我們在請求時, 可能因為 token 過期導致請求 401 導致失敗,你不可能在每個接口調用的地方都判斷一次是否 401 吧。 這種 401 導致的失敗是在封裝請求方法的時候我們就能處理的,具體的請求不需要再去處理, 那我們就不需要把 401 這種錯誤告訴具體的請求使用函數,自己處理就行瞭,我們隻需要告訴它錯誤瞭就行。

但是具體的請求業務錯誤可能就無法處理瞭。舉個常見的例子,比如你獲取一個列表數據,我們在請求時經常和後端約定請求成功碼。經常看到有同學這樣寫:

request()
  .then(res => {
    // 假設請求成功碼 0 為成功
    if (res.code === 0) {
      // ...
    } else if (res.code === 1) {
      // ...
      // 參數錯誤
    } else {
      // ...
      // 其他錯誤
    }
  })

同樣,非常不建議這樣使用,為什麼呢。因為第一點,業務錯誤也是錯誤。then 裡面處理的是請求成功結果,按照剛剛說的,如果處理不瞭錯誤就交由具體使用方去處理,而在這裡業務錯誤應該交由具體請求方的 catch 去處理。所以應該改成:

request()
  .then(res => {
    // then 裡面隻會請求成功,並且能夠成功拿到數據
    // ...
  })
  .catch(err => {
    // 獲取到包裝好的錯誤信息
    if (err.code === 1) {
      // ...
      // 參數錯誤
    } else {
      // ...
      // 其他錯誤
    }
  })

這樣就能確保整個錯誤的傳遞路徑的正確性,而不是不該你處理的錯誤你處理瞭。 具體在 封裝 axios 的時候也有提到,就不展開瞭。

總結

我們先介紹瞭面向錯誤編程的概念,然後講瞭 js 在處理錯誤時的一些方法,以及最後提到瞭錯誤處理該由誰去做的問題。ok,結束。

到此這篇關於詳解JS中異常與錯誤處理的正確方法的文章就介紹到這瞭,更多相關JS異常處理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: