溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

有哪些前端異常處理

發(fā)布時(shí)間:2021-11-02 16:46:10 來源:億速云 閱讀:134 作者:iii 欄目:web開發(fā)

這篇文章主要講解了“有哪些前端異常處理”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“有哪些前端異常處理”吧!

什么是異常

用直白的話來解釋異常的話,就是程序發(fā)生了意想不到的情況,這種情況影響到了程序的正確運(yùn)行。

從根本上來說,異常就是一個(gè)數(shù)據(jù)結(jié)構(gòu),其保存了異常發(fā)生的相關(guān)信息,比如錯(cuò)誤碼,錯(cuò)誤信息等。以 JS 中的標(biāo)準(zhǔn)內(nèi)置對(duì)象 Error 為例,其標(biāo)準(zhǔn)屬性有 name 和 message。然而不同的瀏覽器廠商有自己的自定義屬性,這些屬性并不通用。比如 Mozilla 瀏覽器就增加了 filename 和 stack 等屬性。

值得注意的是錯(cuò)誤只有被拋出,才會(huì)產(chǎn)生異常,不被拋出的錯(cuò)誤不會(huì)產(chǎn)生異常。比如:

function t() {    console.log("start");    new Error();    console.log("end");  }  t();

有哪些前端異常處理

(動(dòng)畫演示)

這段代碼不會(huì)產(chǎn)生任何的異常,控制臺(tái)也不會(huì)有任何錯(cuò)誤輸出。

異常的分類

按照產(chǎn)生異常時(shí)程序是否正在運(yùn)行,我們可以將錯(cuò)誤分為編譯時(shí)異常和運(yùn)行時(shí)異常。

編譯時(shí)異常指的是源代碼在編譯成可執(zhí)行代碼之前產(chǎn)生的異常。而運(yùn)行時(shí)異常指的是可執(zhí)行代碼被裝載到內(nèi)存中執(zhí)行之后產(chǎn)生的異常。

編譯時(shí)異常

我們知道 TS 最終會(huì)被編譯成 JS,從而在 JS Runtime中執(zhí)行。既然存在編譯,就有可能編譯失敗,就會(huì)有編譯時(shí)異常。

比如我使用 TS 寫出了如下代碼:

const s: string = 123;

這很明顯是錯(cuò)誤的代碼, 我給 s 聲明了 string 類型,但是卻給它賦值 number。

當(dāng)我使用 tsc(typescript 編譯工具,全稱是 typescript compiler)嘗試編譯這個(gè)文件的時(shí)候會(huì)有異常拋出:

tsc a.ts  a.ts:1:7 - error TS2322: Type '123' is not assignable to type 'string'.  1 const s: string = 123;          ~  Found 1 error.

這個(gè)異常就是編譯時(shí)異常,因?yàn)槲业拇a還沒有執(zhí)行。

然而并不是你用了 TS 才存在編譯時(shí)異常,JS 同樣有編譯時(shí)異常。有的人可能會(huì)問 JS 不是解釋性語言么?是邊解釋邊執(zhí)行,沒有編譯環(huán)節(jié),怎么會(huì)有編譯時(shí)異常?

別急,我舉個(gè)例子你就明白了。如下代碼:

function t() {    console.log('start')    await sa    console.log('end')  }  t()

上面的代碼由于存在語法錯(cuò)誤,不會(huì)編譯通過,因此并不會(huì)打印start,側(cè)面證明了這是一個(gè)編譯時(shí)異常。盡管 JS 是解釋語言,也依然存在編譯階段,這是必然的,因此自然也會(huì)有編譯異常。

總的來說,編譯異??梢栽诖a被編譯成最終代碼前被發(fā)現(xiàn),因此對(duì)我們的傷害更小。接下來,看一下令人心生畏懼的運(yùn)行時(shí)異常。

運(yùn)行時(shí)異常

相信大家對(duì)運(yùn)行時(shí)異常非常熟悉。這恐怕是廣大前端碰到最多的異常類型了。眾所周知的 NPE(Null Pointer Exception) 就是運(yùn)行時(shí)異常。

將上面的例子稍加改造,得到下面代碼:

function t() {    console.log("start");    throw 1;    console.log("end");  }  t();

有哪些前端異常處理

(動(dòng)畫演示)

注意 end 沒有打印,并且 t 沒有彈出棧。實(shí)際上 t 最終還是會(huì)被彈出的,只不過和普通的返回不一樣。

如上,則會(huì)打印出start。由于異常是在代碼運(yùn)行過程中拋出的,因此這個(gè)異常屬于運(yùn)行時(shí)異常。相對(duì)于編譯時(shí)異常,這種異常更加難以發(fā)現(xiàn)。上面的例子可能比較簡(jiǎn)單,但是如果我的異常是隱藏在某一個(gè)流程控制語句(比如 if else)里面呢?程序就可能在客戶的電腦走入那個(gè)拋出異常的 if 語句,而在你的電腦走入另一條。這就是著名的 《在我電腦上好好的》 事件。

異常的傳播

異常的傳播和我之前寫的瀏覽器事件模型有很大的相似性。只不過那個(gè)是作用在 DOM 這樣的數(shù)據(jù)結(jié)構(gòu),這個(gè)則是作用在函數(shù)調(diào)用棧這種數(shù)據(jù)結(jié)構(gòu),并且事件傳播存在捕獲階段,異常傳播是沒有的。不同 C 語言,JS 中異常傳播是自動(dòng)的,不需要程序員手動(dòng)地一層層傳遞。如果一個(gè)異常沒有被 catch,它會(huì)沿著函數(shù)調(diào)用棧一層層傳播直到???。

異常處理中有兩個(gè)關(guān)鍵詞,它們是throw(拋出異常) 和 catch(處理異常)。 當(dāng)一個(gè)異常被拋出的時(shí)候,異常的傳播就開始了。異常會(huì)不斷傳播直到遇到第一個(gè) catch。 如果程序員沒有手動(dòng) catch,那么一般而言程序會(huì)拋出類似unCaughtError,表示發(fā)生了一個(gè)異常,并且這個(gè)異常沒有被程序中的任何 catch 語言處理。未被捕獲的異常通常會(huì)被打印在控制臺(tái)上,里面有詳細(xì)的堆棧信息,從而幫助程序員快速排查問題。實(shí)際上我們的程序的目標(biāo)是避免 unCaughtError這種異常,而不是一般性的異常。

一點(diǎn)小前提

由于 JS 的 Error 對(duì)象沒有 code 屬性,只能根據(jù) message 來呈現(xiàn),不是很方便。我這里進(jìn)行了簡(jiǎn)單的擴(kuò)展,后面很多地方我用的都是自己擴(kuò)展的 Error ,而不是原生 JS Error ,不再贅述。

oldError = Error;  Error = function ({ code, message, fileName, lineNumber }) {    error = new oldError(message, fileName, lineNumber);    error.code = code;    return error;  };

手動(dòng)拋出 or 自動(dòng)拋出

異常既可以由程序員自己手動(dòng)拋出,也可以由程序自動(dòng)拋出。

throw new Error(`I'm Exception`);

(手動(dòng)拋出的例子)

a = null;  a.toString(); // Thrown: TypeError: Cannot read property 'toString' of null

(程序自動(dòng)拋出的例子)

自動(dòng)拋出異常很好理解,畢竟我們哪個(gè)程序員沒有看到過程序自動(dòng)拋出的異常呢?

“這個(gè)異常突然就跳出來!嚇我一跳!”,某不知名程序員如是說。

那什么時(shí)候應(yīng)該手動(dòng)拋出異常呢?

一個(gè)指導(dǎo)原則就是你已經(jīng)預(yù)知到程序不能正確進(jìn)行下去了。比如我們要實(shí)現(xiàn)除法,首先我們要考慮的是被除數(shù)為 0 的情況。當(dāng)被除數(shù)為 0 的時(shí)候,我們應(yīng)該怎么辦呢?是拋出異常,還是 return 一個(gè)特殊值?答案是都可以,你自己能區(qū)分就行,這沒有一個(gè)嚴(yán)格的參考標(biāo)準(zhǔn)。 我們先來看下拋出異常,告訴調(diào)用者你的輸入,我處理不了這種情況。

function divide(a, b) {    a = +a;    b = +b; // 轉(zhuǎn)化成數(shù)字    if (!b) {      // 匹配 +0, -0, NaN      throw new Error({        code: 1,        message: "Invalid dividend " + b,      });    }    if (Number.isNaN(a)) {      // 匹配 NaN      throw new Error({        code: 2,        message: "Invalid divisor " + a,      });    }    return a / b;  }

上面代碼會(huì)在兩種情況下拋出異常,告訴調(diào)用者你的輸入我處理不了。由于這兩個(gè)異常都是程序員自動(dòng)手動(dòng)拋出的,因此是可預(yù)知的異常。

剛才說了,我們也可以通過返回值來區(qū)分異常輸入。我們來看下返回值輸入是什么,以及和異常有什么關(guān)系。

異常 or 返回

如果是基于異常形式(遇到不能處理的輸入就拋出異常)。當(dāng)別的代碼調(diào)用divide的時(shí)候,需要自己 catch。

function t() {    try {      divide("foo", "bar");    } catch (err) {      if (err.code === 1) {        return console.log("被除數(shù)必須是除0之外的數(shù)");      }      if (err.code === 2) {        return console.log("除數(shù)必須是數(shù)字");      }      throw new Error("不可預(yù)知的錯(cuò)誤");    }  }

然而就像上面我說的那樣,divide 函數(shù)設(shè)計(jì)的時(shí)候,也完全可以不用異常,而是使用返回值來區(qū)分。

function divide(a, b) {    a = +a;    b = +b; // 轉(zhuǎn)化成數(shù)字    if (!b) {      // 匹配 +0, -0, NaN      return new Error({        code: 1,        message: "Invalid dividend " + b,      });    }    if (Number.isNaN(a)) {      // 匹配 NaN      return new Error({        code: 2,        message: "Invalid divisor " + a,      });    }    return a / b;  }

當(dāng)然,我們使用方式也要作出相應(yīng)改變。

function t() {    const res = divide("foo", "bar");    if (res.code === 1) {      return console.log("被除數(shù)必須是除0之外的數(shù)");    }   if (res.code === 2) {      return console.log("除數(shù)必須是數(shù)字");    }    return new Error("不可預(yù)知的錯(cuò)誤");  }

這種函數(shù)設(shè)計(jì)方式和拋出異常的設(shè)計(jì)方式從功能上說都是一樣的,只是告訴調(diào)用方的方式不同。如果你選擇第二種方式,而不是拋出異常,那么實(shí)際上需要調(diào)用方書寫額外的代碼,用來區(qū)分正常情況和異常情況,這并不是一種良好的編程習(xí)慣。

然而在 Go 等返回值可以為復(fù)數(shù)的語言中,我們無需使用上面蹩腳的方式,而是可以:

res, err := divide("foo", "bar");  if err != nil {      log.Fatal(err)  }

這是和 Java 和 JS 等語言使用的 try catch 不一樣的的地方,Go 是通過 panic recover defer 機(jī)制來進(jìn)行異常處理的。感興趣的可以去看看 Go 源碼關(guān)于錯(cuò)誤測(cè)試部分

可能大家對(duì) Go 不太熟悉。沒關(guān)系,我們來繼續(xù)看下 shell。實(shí)際上 shell 也是通過返回值來處理異常的,我們可以通過 $? 拿到上一個(gè)命令的返回值,這本質(zhì)上也是一種調(diào)用棧的傳播行為,而且是通過返回值而不是捕獲來處理異常的。

作為函數(shù)返回值處理和 try catch 一樣,這是語言的設(shè)計(jì)者和開發(fā)者共同決定的一件事情。

上面提到了異常傳播是作用在函數(shù)調(diào)用棧上的。當(dāng)一個(gè)異常發(fā)生的時(shí)候,其會(huì)沿著函數(shù)調(diào)用棧逐層返回,直到第一個(gè) catch 語句。當(dāng)然 catch 語句內(nèi)部仍然可以觸發(fā)異常(自動(dòng)或者手動(dòng))。如果 catch 語句內(nèi)部發(fā)生了異常,也一樣會(huì)沿著其函數(shù)調(diào)用棧繼續(xù)執(zhí)行上述邏輯,專業(yè)術(shù)語是 stack unwinding。

實(shí)際上并不是所有的語言都會(huì)進(jìn)行 stack unwinding,這個(gè)我們會(huì)在接下來的《運(yùn)行時(shí)異??梢曰謴?fù)么?》部分講解。

有哪些前端異常處理

偽代碼來描述一下:

function bubble(error, fn) {    if (fn.hasCatchBlock()) {      runCatchCode(error);    }    if (callstack.isNotEmpty()) {      bubble(error, callstack.pop());    }  }

從我的偽代碼可以看出所謂的 stack unwinding 其實(shí)就是 callstack.pop()

這就是異常傳播的一切!僅此而已。

異常的處理

我們已經(jīng)了解來異常的傳播方式了。那么接下來的問題是,我們應(yīng)該如何在這個(gè)傳播過程中處理異常呢?

我們來看一個(gè)簡(jiǎn)單的例子:

function a() {    b();  }  function b() {    c();  }  function c() {    throw new Error("an error  occured");  }  a();

我們將上面的代碼放到 chrome 中執(zhí)行, 會(huì)在控制臺(tái)顯示如下輸出:

有哪些前端異常處理

我們可以清楚地看出函數(shù)的調(diào)用關(guān)系。即錯(cuò)誤是在 c 中發(fā)生的,而 c 是 b 調(diào)用的,b 是 a 調(diào)用的。這個(gè)函數(shù)調(diào)用棧是為了方便開發(fā)者定位問題而存在的。

上面的代碼,我們并沒有 catch 錯(cuò)誤,因此上面才會(huì)有uncaught Error。

那么如果我們 catch ,會(huì)發(fā)生什么樣的變化呢?catch 的位置會(huì)對(duì)結(jié)果產(chǎn)生什么樣的影響?在 a ,b,c 中 catch 的效果是一樣的么?

我們來分別看下:

function a() {    b();  }  function b() {    c();  }  function c() {    try {      throw new Error("an error  occured");    } catch (err) {      console.log(err);    }  }  a();

(在 c 中 catch)

我們將上面的代碼放到 chrome 中執(zhí)行, 會(huì)在控制臺(tái)顯示如下輸出:

有哪些前端異常處理

可以看出,此時(shí)已經(jīng)沒有uncaught Error啦,僅僅在控制臺(tái)顯示了標(biāo)準(zhǔn)輸出,而非錯(cuò)誤輸出(因?yàn)槲矣玫氖?console.log,而不是 console.error)。然而更重要是的是,如果我們沒有 catch,那么后面的同步代碼將不會(huì)執(zhí)行。

比如在 c 的 throw 下面增加一行代碼,這行代碼是無法被執(zhí)行的,無論這個(gè)錯(cuò)誤有沒有被捕獲。

function c() {    try {      throw new Error("an error  occured");      console.log("will never run");    } catch (err) {      console.log(err);    }  }

我們將 catch 移動(dòng)到 b 中試試看。

function a() {    b();  }  function b() {    try {      c();    } catch (err) {      console.log(err);    }  }  function c() {    throw new Error("an error  occured");  }  a();

(在 b 中 catch)

在這個(gè)例子中,和上面在 c 中捕獲沒有什么本質(zhì)不同。其實(shí)放到 a 中捕獲也是一樣,這里不再貼代碼了,感興趣的自己試下。

既然處于函數(shù)調(diào)用棧頂部的函數(shù)報(bào)錯(cuò), 其函數(shù)調(diào)用棧下方的任意函數(shù)都可以進(jìn)行捕獲,并且效果沒有本質(zhì)不同。那么問題來了,我到底應(yīng)該在哪里進(jìn)行錯(cuò)誤處理呢?

答案是責(zé)任鏈模式。我們先來簡(jiǎn)單介紹一下責(zé)任鏈模式,不過細(xì)節(jié)不會(huì)在這里展開。

假如 lucifer 要請(qǐng)假。

  •  如果請(qǐng)假天數(shù)小于等于 1 天,則主管同意即可

  •  如果請(qǐng)假大于 1 天,但是小于等于三天,則需要 CTO 同意。

  •  如果請(qǐng)假天數(shù)大于三天,則需要老板同意。

有哪些前端異常處理

這就是一個(gè)典型的責(zé)任鏈模式。誰有責(zé)任干什么事情是確定的,不要做自己能力范圍之外的事情。比如主管不要去同意大于 1 天的審批。

舉個(gè)例子,假設(shè)我們的應(yīng)用有三個(gè)異常處理類,它們分別是:用戶輸入錯(cuò)誤,網(wǎng)絡(luò)錯(cuò)誤 和 類型錯(cuò)誤。如下代碼,當(dāng)代碼執(zhí)行的時(shí)候會(huì)報(bào)錯(cuò)一個(gè)用戶輸入異常。這個(gè)異常沒有被 C 捕獲,會(huì) unwind stack 到 b,而 b 中 catch 到這個(gè)錯(cuò)誤之后,通過查看 code 值判斷其可以被處理,于是打印I can handle this。

function a() {    try {      b();    } catch (err) {      if (err.code === "NETWORK_ERROR") {        return console.log("I can handle this");      }      // can't handle, pass it down      throw err;    }  }  function b() {    try {      c();    } catch (err) {      if (err.code === "INPUT_ERROR") {        return console.log("I can handle this");      }      // can't handle, pass it down      throw err;    }  }  function c() {    throw new Error({      code: "INPUT_ERROR",      message: "an error  occured",    });  }  a();

而如果 c 中拋出的是別的異常,比如網(wǎng)絡(luò)異常,那么 b 是無法處理的,雖然 b catch 住了,但是由于你無法處理,因此一個(gè)好的做法是繼續(xù)拋出異常,而不是吞沒異常。不要畏懼錯(cuò)誤,拋出它。只有沒有被捕獲的異常才是可怕的,如果一個(gè)錯(cuò)誤可以被捕獲并得到正確處理,它就不可怕。

舉個(gè)例子:

function a() {    try {      b();    } catch (err) {      if (err.code === "NETWORK_ERROR") {        return console.log("I can handle this");      }      // can't handle, pass it down      throw err;    }  }  function b() {    try {      c();    } catch (err) {      if (err.code === "INPUT_ERROR") {        return console.log("I can handle this");      }    }  }  function c() {    throw new Error({      code: "NETWORK_ERROR",      message: "an error  occured",    });  }  a();

如上代碼不會(huì)有任何異常被拋出,它被完全吞沒了,這對(duì)我們調(diào)試問題簡(jiǎn)直是災(zāi)難。因此切記不要吞沒你不能處理的異常。正確的做法應(yīng)該是上面講的那種只 catch 你可以處理的異常,而將你不能處理的異常 throw 出來,這就是責(zé)任鏈模式的典型應(yīng)用。

這只是一個(gè)簡(jiǎn)單的例子,就足以繞半天。實(shí)際業(yè)務(wù)肯定比這個(gè)復(fù)雜多得多。因此異常處理絕對(duì)不是一件容易的事情。

如果說誰來處理是一件困難的事情,那么在異步中決定誰來處理異常就是難上加難,我們來看下。

同步與異步

同步異步一直是前端難以跨越的坎,對(duì)于異常處理也是一樣。以 NodeJS 中用的比較多的讀取文件 API 為例。它有兩個(gè)版本,一個(gè)是異步,一個(gè)是同步。同步讀取僅僅應(yīng)該被用在沒了這個(gè)文件無法進(jìn)行下去的時(shí)候。比如讀取一個(gè)配置文件。而不應(yīng)該在比如瀏覽器中讀取用戶磁盤上的一個(gè)圖片等,這樣會(huì)造成主線程阻塞,導(dǎo)致瀏覽器卡死。

// 異步讀取文件  fs.readFileSync();  // 同步讀取文件  fs.readFile();

當(dāng)我們?cè)噲D同步讀取一個(gè)不存在的文件的時(shí)候,會(huì)拋出以下異常:

fs.readFileSync('something-not-exist.lucifer');  console.log('腦洞前端');  Thrown:  Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'      at Object.openSync (fs.js:446:3)      at Object.readFileSync (fs.js:348:35) {    errno: -2,    syscall: 'open',    code: 'ENOENT',    path: 'something-not-exist.lucifer'  }

并且腦洞前端是不會(huì)被打印出來的。這個(gè)比較好理解,我們上面已經(jīng)解釋過了。

而如果以異步方式的話:

fs.readFile('something-not-exist.lucifer', (err, data) => {if(err) {throw err}});  console.log('lucifer')  lucifer  undefined  Thrown:  [Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'] {    errno: -2,    code: 'ENOENT',    syscall: 'open',    path: 'something-not-exist.lucifer'  }  >

腦洞前端是會(huì)被打印出來的。

其本質(zhì)在于 fs.readFile 的函數(shù)調(diào)用已經(jīng)成功,并從調(diào)用棧返回并執(zhí)行到下一行的console.log('lucifer')。因此錯(cuò)誤發(fā)生的時(shí)候,調(diào)用棧是空的,這一點(diǎn)可以從上面的錯(cuò)誤堆棧信息中看出來。

不明白為什么調(diào)用棧是空的同學(xué)可以看下我之前寫的《一文看懂瀏覽器事件循環(huán)》

而 try catch 的作用僅僅是捕獲當(dāng)前調(diào)用棧的錯(cuò)誤(上面異常傳播部分已經(jīng)講過了)。因此異步的錯(cuò)誤是無法捕獲的,比如;

try {    fs.readFile("something-not-exist.lucifer", (err, data) => {      if (err) {        throw err;      }    });  } catch (err) {    console.log("catching an error");  }

上面的 catching an error 不會(huì)被打印。因?yàn)殄e(cuò)誤拋出的時(shí)候, 調(diào)用棧中不包含這個(gè) catch 語句,而僅僅在執(zhí)行fs.readFile的時(shí)候才會(huì)。

如果我們換成同步讀取文件的例子看看:

try {    fs.readFileSync("something-not-exist.lucifer");  } catch (err) {   console.log("catching an error");  }

上面的代碼會(huì)打印 catching an error。因?yàn)樽x取文件被同步發(fā)起,文件返回之前線程會(huì)被掛起,當(dāng)線程恢復(fù)執(zhí)行的時(shí)候, fs.readFileSync 仍然在函數(shù)調(diào)用棧中,因此 fs.readFileSync 產(chǎn)生的異常會(huì)冒泡到 catch 語句。

簡(jiǎn)單來說就是異步產(chǎn)生的錯(cuò)誤不能用 try catch 捕獲,而要使用回調(diào)捕獲。

可能有人會(huì)問了,我見過用 try catch 捕獲異步異常啊。 比如:

rejectIn = (ms) =>    new Promise((_, r) => {      setTimeout(() => {        r(1);      }, ms);    });  async function t() {    try {      await rejectIn(0);    } catch (err) {      console.log("catching an error", err);    }  }  t();

本質(zhì)上這只是一個(gè)語法糖,是 Promise.prototype.catch 的一個(gè)語法糖而已。而這一語法糖能夠成立的原因在于其用了 Promise 這種包裝類型。如果你不用包裝類型,比如上面的 fs.readFile 不用 Promise 等包裝類型包裝,打死都不能用 try catch 捕獲。

而如果我們使用 babel 轉(zhuǎn)義下,會(huì)發(fā)現(xiàn) try catch 不見了,變成了 switch case 語句。這就是 try catch “可以捕獲異步異?!钡脑?,僅此而已,沒有更多。

有哪些前端異常處理

(babel 轉(zhuǎn)義結(jié)果)

我使用的 babel 轉(zhuǎn)義環(huán)境都記錄在這里,大家可以直接點(diǎn)開鏈接查看.

雖然瀏覽器并不像 babel 轉(zhuǎn)義這般實(shí)現(xiàn),但是至少我們明白了一點(diǎn)。目前的 try catch 的作用機(jī)制是無法捕獲異步異常的。

異步的錯(cuò)誤處理推薦使用容器包裝,比如 Promise。然后使用 catch 進(jìn)行處理。實(shí)際上 Promise 的 catch 和 try catch 的 catch 有很多相似的地方,大家可以類比過去。

和同步處理一樣,很多原則都是通用的。比如異步也不要去吞沒異常。下面的代碼是不好的,因?yàn)樗虥]了它不能處理的異常。

p = Promise.reject(1);  p.catch(() => {});

更合適的做法的應(yīng)該是類似這種:

p = Promise.reject(1);  p.catch((err) => {    if (err == 1) {      return console.log("I can handle this");    }    throw err;  });

徹底消除運(yùn)行時(shí)異常可能么?

我個(gè)人對(duì)目前前端現(xiàn)狀最為頭疼的一點(diǎn)是:大家過分依賴運(yùn)行時(shí),而嚴(yán)重忽略編譯時(shí)。我見過很多程序,你如果不運(yùn)行,根本不知道程序是怎么走的,每個(gè)變量的 shape 是什么。怪不得處處都可以看到 console.log。我相信你一定對(duì)此感同身受。也許你就是那個(gè)寫出這種代碼的人,也許你是給別人擦屁股的人。為什么會(huì)這樣? 就是因?yàn)榇蠹姨蕾囘\(yùn)行時(shí)。TS 的出現(xiàn)很大程度上改善了這一點(diǎn),前提是你用的是 typescript,而不是 anyscript。其實(shí) eslint 以及 stylint 對(duì)此也有貢獻(xiàn),畢竟它們都是靜態(tài)分析工具。

我強(qiáng)烈建議將異常保留在編譯時(shí),而不是運(yùn)行時(shí)。不妨極端一點(diǎn)來看:假如所有的異常都在編譯時(shí)發(fā)生,而一定不會(huì)在運(yùn)行時(shí)發(fā)生。那么我們是不是就可以信心滿滿地對(duì)應(yīng)用進(jìn)行重構(gòu)啦?

幸運(yùn)的是,我們能夠做到。只不過如果當(dāng)前語言做不到的話,則需要對(duì)現(xiàn)有的語言體系進(jìn)行改造。這種改造成本真的很大。不僅僅是 API,編程模型也發(fā)生了翻天覆地的變化,不然函數(shù)式也不會(huì)這么多年沒有得到普及了。

不熟悉函數(shù)編程的可以看看我之前寫的函數(shù)式編程入門篇。

如果才能徹底消除異常呢?在回答這個(gè)問題之前,我們先來看下一門號(hào)稱沒有運(yùn)行時(shí)異常的語言 elm。elm 是一門可以編譯為 JS 的函數(shù)式編程語言,其封裝了諸如網(wǎng)絡(luò) IO 等副作用,是一種聲明式可推導(dǎo)的語言。 有趣的是,elm 也有異常處理。 elm 中關(guān)于異常處理(Error Handling)部分有兩個(gè)小節(jié)的內(nèi)容,分別是:Maybe 和 Result。elm 之所以沒有運(yùn)行時(shí)異常的一個(gè)原因就是它們。 一句話概括“為什么 elm 沒有異?!钡脑?,那就是elm 把異??醋鲾?shù)據(jù)(data)。

舉個(gè)簡(jiǎn)單的例子:

maybeResolveOrNot = (ms) =>    setTimeout(() => {      if (Math.random() > 0.5) {        console.log("ok");      } else {        throw new Error("error");      }    });

上面的代碼有一半的可能報(bào)錯(cuò)。那么在 elm 中就不允許這樣的情況發(fā)生。所有的可能發(fā)生異常的代碼都會(huì)被強(qiáng)制包裝一層容器,這個(gè)容器在這里是 Maybe。

有哪些前端異常處理

在其他函數(shù)式編程語言名字可能有所不同,但是意義相同。實(shí)際上,不僅僅是異常,正常的數(shù)據(jù)也會(huì)被包裝到容器中,你需要通過容器的接口來獲取數(shù)據(jù)。如果難以理解的話,你可以將其簡(jiǎn)單理解為 Promsie(但并不完全等價(jià))。

Maybe 可能返回正常的數(shù)據(jù) data,也可能會(huì)生成一個(gè)錯(cuò)誤 error。某一個(gè)時(shí)刻只能是其中一個(gè),并且只有運(yùn)行的時(shí)候,我們才真正知道它是什么。從這一點(diǎn)來看,有點(diǎn)像薛定諤的貓。

有哪些前端異常處理

不過 Maybe 已經(jīng)完全考慮到異常的存在,一切都在它的掌握之中。所有的異常都能夠在編譯時(shí)推導(dǎo)出來。當(dāng)然要想推導(dǎo)出這些東西,你需要對(duì)整個(gè)編程模型做一定的封裝會(huì)抽象,比如 DOM 就不能直接用了,而是需要一個(gè)中間層。

再來看下一個(gè)更普遍的例子 NPE:

null.toString();

elm 也不會(huì)發(fā)生。原因也很簡(jiǎn)單,因?yàn)?null 也會(huì)被包裝起來,當(dāng)你通過這個(gè)包裝類型就行訪問的時(shí)候,容器有能力避免這種情況,因此就可以不會(huì)發(fā)生異常。當(dāng)然這里有一個(gè)很重要的前提就是可推導(dǎo),而這正是函數(shù)式編程語言的特性。這部分內(nèi)容超出了本文的討論范圍,不再這里說了。

運(yùn)行時(shí)異常可以恢復(fù)么?

最后要討論的一個(gè)主題是運(yùn)行時(shí)異常是否可以恢復(fù)。先來解釋一下,什么是運(yùn)行時(shí)異常的恢復(fù)。 還是用上面的例子:

function t() {    console.log("start");    throw 1;    console.log("end");  }  t();

這個(gè)我們已經(jīng)知道了, end 是不會(huì)打印的。 盡管你這么寫也是無濟(jì)于事:

function t() {    try {      console.log("start");      throw 1;      console.log("end");    } catch (err) {      console.log("relax, I can handle this");    }  }  t();

如果我想讓它打印呢?我想讓程序面對(duì)異??梢宰约?recover 怎么辦?我已經(jīng)捕獲這個(gè)錯(cuò)誤, 并且我確信我可以處理,讓流程繼續(xù)走下去吧!如果有能力做到這個(gè),這個(gè)就是運(yùn)行時(shí)異?;謴?fù)。

遺憾地告訴你,據(jù)我所知,目前沒有任何一個(gè)引擎能夠做到這一點(diǎn)。

這個(gè)例子過于簡(jiǎn)單, 只能幫助我們理解什么是運(yùn)行時(shí)異?;謴?fù),但是不足以讓我們看出這有什么用?

有哪些前端異常處理

我們來看一個(gè)更加復(fù)雜的例子,我們這里直接使用上面實(shí)現(xiàn)過的函數(shù)divide。

function t() {    try {      const res = divide("foo", "bar");      alert(`you got ${res}`);    } catch (err) {      if (err.code === 1) {        return console.log("被除數(shù)必須是除0之外的數(shù)");      }      if (err.code === 2) {        return console.log("除數(shù)必須是數(shù)字");      }      throw new Error("不可預(yù)知的錯(cuò)誤");    }  }

如上代碼,會(huì)進(jìn)入 catch ,而不會(huì) alert。因此對(duì)于用戶來說, 應(yīng)用程序是沒有任何響應(yīng)的。這是不可接受的。

要吐槽一點(diǎn)的是這種事情真的是挺常見的,只不過大家用的不是 alert 罷了。

如果我們的代碼在進(jìn)入 catch 之后還能夠繼續(xù)返回出錯(cuò)位置繼續(xù)執(zhí)行就好了。

有哪些前端異常處理

如何實(shí)現(xiàn)異常中斷的恢復(fù)呢?我剛剛說了:據(jù)我所知,目前沒有任何一個(gè)引擎能夠做到異常恢復(fù)。那么我就來發(fā)明一個(gè)新的語法解決這個(gè)問題。

function t() {    try {      const res = divide("foo", "bar");      alert(`you got ${res}`);    } catch (err) {      console.log("releax, I can handle this");      resume - 1;    }  }  t();

上面的 resume 是我定義的一個(gè)關(guān)鍵字,功能是如果遇到異常,則返回到異常發(fā)生的地方,然后給當(dāng)前發(fā)生異常的函數(shù)一個(gè)返回值 -1,并使得后續(xù)代碼能夠正常運(yùn)行,不受影響。這其實(shí)是一種 fallback。

這絕對(duì)是一個(gè)超前的理念。當(dāng)然挑戰(zhàn)也非常大,對(duì)現(xiàn)有的體系沖擊很大,很多東西都要改。我希望社區(qū)可以考慮把這個(gè)東西加到標(biāo)準(zhǔn)。

最佳實(shí)踐

通過前面的學(xué)習(xí),你已經(jīng)知道了異常是什么,異常是怎么產(chǎn)生的,以及如何正確處理異常(同步和異步)。接下來,我們談一下異常處理的最佳實(shí)踐。

我們平時(shí)開發(fā)一個(gè)應(yīng)用。 如果站在生產(chǎn)者和消費(fèi)者的角度來看的話。當(dāng)我們使用別人封裝的框架,庫,模塊,甚至是函數(shù)的時(shí)候,我們就是消費(fèi)者。而當(dāng)我們寫的東西被他人使用的時(shí)候,我們就是生產(chǎn)者。

實(shí)際上,就算是生產(chǎn)者內(nèi)部也會(huì)有多個(gè)模塊構(gòu)成,多個(gè)模塊之間也會(huì)有生產(chǎn)者和消費(fèi)者的再次身份轉(zhuǎn)化。不過為了簡(jiǎn)單起見,本文不考慮這種關(guān)系。這里的生產(chǎn)者指的就是給他人使用的功能,是純粹的生產(chǎn)者。

從這個(gè)角度出發(fā),來看下異常處理的最佳實(shí)踐。

作為消費(fèi)者

當(dāng)作為消費(fèi)者的時(shí)候,我們關(guān)心的是使用的功能是否會(huì)拋出異常,如果是,他們有哪些異常。比如:

import foo from "lucifer";  try {    foo.bar();  } catch (err) {    // 有哪些異常?  }

當(dāng)然,理論上 foo.bar 可能產(chǎn)生任何異常,而不管它的 API 是這么寫的。但是我們關(guān)心的是可預(yù)期的異常。因此你一定希望這個(gè)時(shí)候有一個(gè) API 文檔,詳細(xì)列舉了這個(gè) API 可能產(chǎn)生的異常有哪些。

比如這個(gè) foo.bar 4 種可能的異常 分別是 A,B,C 和 D。其中 A 和 B 是我可以處理的,而 C 和 D 是我不能處理的。那么我應(yīng)該:

import foo from "lucifer";  try {    foo.bar();  } catch (err) {    if (err.code === "A") {      return console.log("A happened");    }    if (err.code === "B") {      return console.log("B happened");    }    throw err;  }

可以看出,不管是 C 和 D,還是 API 中沒有列舉的各種可能異常,我們的做法都是直接拋出。

有哪些前端異常處理

作為生產(chǎn)者

如果你作為生產(chǎn)者,你要做的就是提供上面提到的詳細(xì)的 API,告訴消費(fèi)者你的可能錯(cuò)誤有哪些。這樣消費(fèi)者就可以在 catch 中進(jìn)行相應(yīng)判斷,處理異常情況。

有哪些前端異常處理

感謝各位的閱讀,以上就是“有哪些前端異常處理”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)有哪些前端異常處理這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI