Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
T之家 7 月 11 日消息,微軟公司在 7 月補(bǔ)丁星期二發(fā)布的 Windows 10、Windows 11 系統(tǒng)累積更新中,修復(fù)了追蹤編號(hào)為 CVE-2024-38112 的零日漏洞。
該零日漏洞由 Check Point Research 的安全專家李海飛(Haifei Li,音譯)于 2023 年 1 月發(fā)現(xiàn),是一個(gè)高度嚴(yán)重的 MHTML 欺騙問題,有證據(jù)表明有黑客在過去 18 個(gè)月里,利用該漏洞發(fā)起惡意攻擊,可以繞過 Windows 10、Windows 11 系統(tǒng)的安全功能。
該專家發(fā)現(xiàn)網(wǎng)絡(luò)攻擊者通過分發(fā) Windows Internet 快捷方式文件(.url),以欺騙 PDF 等看起來合法的文件,用戶一旦點(diǎn)開這些文件,就會(huì)下載并啟動(dòng) HTA 以安裝密碼竊取惡意軟件。
Internet 快捷方式文件只是一個(gè)文本文件,其中包含各種配置設(shè)置,如顯示什么圖標(biāo)、雙擊時(shí)打開什么鏈接等信息。保存為 .url 文件并雙擊后,Windows 將在默認(rèn)網(wǎng)絡(luò)瀏覽器中打開配置的 URL。
不過攻擊者發(fā)現(xiàn)可以通過在 URL 指令中使用 mhtml: URI 處理程序,來強(qiáng)制 Internet Explorer 打開指定的 URL,如下圖所示:
IT之家注:MHTML 是一種 "聚合 HTML 文檔的 MIME 封裝" 文件,是 Internet Explorer 中引入的一種技術(shù),可將包括圖像在內(nèi)的整個(gè)網(wǎng)頁封裝成一個(gè)單一的檔案。
攻擊者使用 mhtml: URI 啟動(dòng) URL 后,Windows 會(huì)自動(dòng)在 Internet Explorer 中啟動(dòng) URL,而不是默認(rèn)瀏覽器。
漏洞研究人員 Will Dormann 稱,在 Internet Explorer 中打開網(wǎng)頁會(huì)給攻擊者帶來額外的好處,下載惡意文件時(shí)安全警告較少。
盡管微軟早在兩年前就宣布停止支持該瀏覽器,并以 Edge 代替其所有實(shí)用功能,但這款過時(shí)的瀏覽器仍可被惡意調(diào)用和利用。
家好,我是 Echa。
本文將帶你了解 JavaScript 中常見的錯(cuò)誤類型,處理同步和異步 JavaScript/Node.js 代碼中錯(cuò)誤和異常的方式,以及錯(cuò)誤處理最佳實(shí)踐!
JavaScript 中的錯(cuò)誤是一個(gè)對(duì)象,在發(fā)生錯(cuò)誤時(shí)會(huì)拋出該對(duì)象以停止程序。在 JavaScript 中,可以通過構(gòu)造函數(shù)來創(chuàng)建一個(gè)新的通用錯(cuò)誤:
const err = new Error("Error");
當(dāng)然,也可以省略 new 關(guān)鍵字:
const err = Error("Error");
Error 對(duì)象有三個(gè)屬性:
例如,創(chuàng)建一個(gè) TypeError 對(duì)象,該消息將攜帶實(shí)際的錯(cuò)誤字符串,其 name 將是“TypeError”:
const wrongType = TypeError("Expected number");
wrongType.message; // 'Expected number'
wrongType.name; // 'TypeError'
堆棧跟蹤是發(fā)生異常或警告等事件時(shí)程序所處的方法調(diào)用列表:
它首先會(huì)打印錯(cuò)誤名稱和消息,然后是被調(diào)用的方法列表。每個(gè)方法調(diào)用都說明其源代碼的位置和調(diào)用它的行。可以使用此數(shù)據(jù)來瀏覽代碼庫并確定導(dǎo)致錯(cuò)誤的代碼段。此方法列表以堆疊的方式排列。它顯示了異常首先被拋出的位置以及它如何通過堆棧方法調(diào)用傳播。為異常實(shí)施捕獲不會(huì)讓它通過堆棧向上傳播并使程序崩潰。
對(duì)于 Error 對(duì)象,F(xiàn)irefox 還實(shí)現(xiàn)了一些非標(biāo)準(zhǔn)屬性:
JavaScript 中有一系列預(yù)定義的錯(cuò)誤類型。只要使用者沒有明確處理應(yīng)用程序中的錯(cuò)誤,它們就會(huì)由 JavaScript 運(yùn)行時(shí)自動(dòng)選擇和定義。
JavaScript中的錯(cuò)誤類型包括:
這些錯(cuò)誤類型都是實(shí)際的構(gòu)造函數(shù),旨在返回一個(gè)新的錯(cuò)誤對(duì)象。最常見的就是 TypeError。大多數(shù)時(shí)候,大部分錯(cuò)誤將直接來自 JavaScript 引擎,例如 InternalError 或 SyntaxError。
JavaScript 提供了 instanceof 運(yùn)算符可以用于區(qū)分異常類型:
try {
If (typeof x !== ‘number’) {
throw new TypeError(‘x 應(yīng)是數(shù)字’);
} else if (x <= 0) {
throw new RangeError('x 應(yīng)大于 0');
} else {
// ...
}
} catch (err) {
if (err instanceof TypeError) {
// 處理 TypeError 錯(cuò)誤
} else if (err instanceof RangeError) {
// 處理 RangeError 錯(cuò)誤
} else {
// 處理其他類型錯(cuò)誤
}
}
下面來了解 JavaScript 中最常見的錯(cuò)誤類型,并了解它們發(fā)生的時(shí)間和原因。
SyntaxError 表示語法錯(cuò)誤。這些錯(cuò)誤是最容易修復(fù)的錯(cuò)誤之一,因?yàn)樗鼈儽砻鞔a語法中存在錯(cuò)誤。由于 JavaScript 是一種解釋而非編譯的腳本語言,因此當(dāng)應(yīng)用程序執(zhí)行包含錯(cuò)誤的腳本時(shí)會(huì)拋出這些錯(cuò)誤。在編譯語言的情況下,此類錯(cuò)誤在編譯期間被識(shí)別。因此,在修復(fù)這些問題之前,不會(huì)創(chuàng)建應(yīng)用程序二進(jìn)制文件。
SyntaxError 發(fā)生的一些常見原因是:
TypeError 是 JavaScript 應(yīng)用程序中最常見的錯(cuò)誤之一,當(dāng)某些值不是特定的預(yù)期類型時(shí),就會(huì)產(chǎn)生此錯(cuò)誤。
TypeError 發(fā)生的一些常見原因是:
ReferenceError 表示引用錯(cuò)誤。當(dāng)代碼中的變量引用有問題時(shí),會(huì)發(fā)生 ReferenceError。可能忘記在使用變量之前為其定義一個(gè)值,或者可能試圖在代碼中使用一個(gè)不可訪問的變量。在任何情況下,通過堆棧跟蹤都可以提供充足的信息來查找和修復(fù)有問題的變量引用。
ReferenceErrors 發(fā)生的一些常見原因如下:
RangeError 表示范圍錯(cuò)誤。當(dāng)變量設(shè)置的值超出其合法值范圍時(shí),將拋出 RangeError。它通常發(fā)生在將值作為參數(shù)傳遞給函數(shù)時(shí),并且給定值不在函數(shù)參數(shù)的范圍內(nèi)。當(dāng)使用記錄不完整的第三方庫時(shí),有時(shí)修復(fù)起來會(huì)很棘手,因?yàn)樾枰绤?shù)的可能值范圍才能傳遞正確的值。
RangeError 發(fā)生的一些常見場(chǎng)景如下:
URIError 表示 URI錯(cuò)誤。當(dāng) URI 的編碼和解碼出現(xiàn)問題時(shí),會(huì)拋出 URIError。JavaScript 中的 URI 操作函數(shù)包括:decodeURI、decodeURIComponent 等。如果使用了錯(cuò)誤的參數(shù)(無效字符),就會(huì)拋出 URIError。
EvalError 表示 Eval 錯(cuò)誤。當(dāng) eval() 函數(shù)調(diào)用發(fā)生錯(cuò)誤時(shí),會(huì)拋出 EvalError。不過,當(dāng)前的 JavaScript 引擎或 ECMAScript 規(guī)范不再拋出此錯(cuò)誤。但是,為了向后兼容,它仍然是存在的。
如果使用的是舊版本的 JavaScript,可能會(huì)遇到此錯(cuò)誤。在任何情況下,最好調(diào)查在eval()函數(shù)調(diào)用中執(zhí)行的代碼是否有任何異常。
InternalError 表示內(nèi)部錯(cuò)誤。在 JavaScript 運(yùn)行時(shí)引擎發(fā)生異常時(shí)使用。它表示代碼可能存在問題也可能不存在問題。
InternalError 通常只發(fā)生在兩種情況下:
解決此錯(cuò)誤最合適的方法就是通過錯(cuò)誤消息確定原因,并在可能的情況下重構(gòu)應(yīng)用邏輯,以消除 JavaScript 引擎上工作負(fù)載的突然激增。
注意: 現(xiàn)代 JavaScript 中不會(huì)拋出 EvalError 和 InternalError。
雖然 JavaScript 提供了足夠的錯(cuò)誤類型類列表來涵蓋大多數(shù)情況,但如果這些錯(cuò)誤類型不能滿足要求,還可以創(chuàng)建新的錯(cuò)誤類型。這種靈活性的基礎(chǔ)在于 JavaScript 允許使用 throw 命令拋出任何內(nèi)容。
可以通過擴(kuò)展 Error 類以創(chuàng)建自定義錯(cuò)誤類:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
可以通過以下方式使用它:
throw ValidationError("未找到該屬性: name")
可以使用 instanceof 關(guān)鍵字識(shí)別它:
try {
validateForm() // 拋出 ValidationError 的代碼
} catch (e) {
if (e instanceof ValidationError) {
}
else {
}
}
很多人認(rèn)為錯(cuò)誤和異常是一回事。實(shí)際上,Error 對(duì)象只有在被拋出時(shí)才會(huì)成為異常。
在 JavaScript 中拋出異常,可以使用 throw 來拋出 Error 對(duì)象:
throw TypeError("Expected number");
或者:
throw new TypeError("Expected number");
來看一個(gè)簡單的例子:
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Expected string");
}
return string.toUpperCase();
}
在這里,我們檢查函數(shù)參數(shù)是否為字符串。如果不是,就拋出異常。
從技術(shù)上講,我們可以在 JavaScript 中拋出任何東西,而不僅僅是 Error 對(duì)象:
throw Symbol();
throw 33;
throw "Error!";
throw null;
但是,最好避免這樣做:要拋出正確的 Error 對(duì)象,而不是原語。
異常一旦拋出,就會(huì)在程序堆棧中冒泡,除非在某個(gè)地方被捕獲。
來看下面的例子:
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Expected string");
}
return string.toUpperCase();
}
toUppercase(4);
在瀏覽器或 Node.js 中運(yùn)行此代碼,程序?qū)⑼V共伋鲥e(cuò)誤:
這里還顯示了發(fā)生錯(cuò)誤的確切行。這個(gè)錯(cuò)誤就是一個(gè)堆棧跟蹤,有助于跟蹤代碼中的問題。堆棧跟蹤從下到上:
at toUppercase (<anonymous>:3:11)
at <anonymous>:9:1
toUppercase 函數(shù)在第 9 行調(diào)用,在第 3 行拋出錯(cuò)誤。除了在瀏覽器的控制臺(tái)中查看此堆棧跟蹤之外,還可以在 Error 對(duì)象的 stack 屬性上訪問它。
介紹完這些關(guān)于錯(cuò)誤的基礎(chǔ)知識(shí)之后,下面來看看同步和異步 JavaScript 代碼中的錯(cuò)誤和異常處理。
同步代碼會(huì)按照代碼編寫順序執(zhí)行。讓我們?cè)倏纯辞懊娴睦樱?/span>
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Expected string");
}
return string.toUpperCase();
}
toUppercase(4);
在這里,引擎調(diào)用并執(zhí)行 toUppercase,這一切都是同步發(fā)生的。 要捕獲由此類同步函數(shù)引發(fā)的異常,可以使用 try/catch/finally:
try {
toUppercase(4);
} catch (error) {
console.error(error.message);
} finally {
// ...
}
通常,try 會(huì)處理正常的路徑,或者可能進(jìn)行的函數(shù)調(diào)用。catch 就會(huì)捕獲實(shí)際的異常,它接收 Error 對(duì)象。而不管函數(shù)的結(jié)果如何,finally 語句都會(huì)運(yùn)行:無論它失敗還是成功,finally 中的代碼都會(huì)運(yùn)行。
JavaScript 中的生成器函數(shù)是一種特殊類型的函數(shù)。它可以隨意暫停和恢復(fù),除了在其內(nèi)部范圍和消費(fèi)者之間提供雙向通信通道。為了創(chuàng)建一個(gè)生成器函數(shù),需要在 function 關(guān)鍵字后面加上一個(gè) *:
function* generate() {
//
}
只要進(jìn)入函數(shù),就可以使用 yield 來返回值:
function* generate() {
yield 33;
yield 99;
}
生成器函數(shù)的返回值是一個(gè)迭代器對(duì)象。要從生成器中提取值,可以使用兩種方法:
以上面的代碼為例,要從生成器中獲取值,可以這樣做:
function* generate() {
yield 33;
yield 99;
}
const go = generate();
當(dāng)我們調(diào)用生成器函數(shù)時(shí),這里的 go 就是生成的迭代器對(duì)象。接下來,就可以調(diào)用 go.next() 來繼續(xù)執(zhí)行:
function* generate() {
yield 33;
yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99
生成器也可以接受來自調(diào)用者的值和異常。除了 next(),從生成器返回的迭代器對(duì)象還有一個(gè) throw() 方法。使用這種方法,就可以通過向生成器中注入異常來停止程序:
function* generate() {
yield 33;
yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
go.throw(Error("Tired of iterating!"));
const secondStep = go.next().value; // never reached
要捕獲此類錯(cuò)誤,可以使用 try/catch 將代碼包裝在生成器中:
function* generate() {
try {
yield 33;
yield 99;
} catch (error) {
console.error(error.message);
}
}
生成器函數(shù)也可以向外部拋出異常。 捕獲這些異常的機(jī)制與捕獲同步異常的機(jī)制相同:try/catch/finally。
下面是使用 for...of 從外部使用的生成器函數(shù)的示例:
function* generate() {
yield 33;
yield 99;
throw Error("Tired of iterating!");
}
try {
for (const value of generate()) {
console.log(value);
}
} catch (error) {
console.error(error.message);
}
輸出結(jié)果如下:
這里,try 塊中包含正常的迭代。如果發(fā)生任何異常,就會(huì)用 catch 捕獲它。
瀏覽器中的異步包括定時(shí)器、事件、Promise 等。異步世界中的錯(cuò)誤處理與同步世界中的處理不同。下面來看一些例子。
上面我們介紹了如何使用 try/catch/finally 來處理錯(cuò)誤,那異步中可以使用這些來處理錯(cuò)誤嗎?先來看一個(gè)例子:
function failAfterOneSecond() {
setTimeout(() => {
throw Error("Wrong!");
}, 1000);
}
此函數(shù)在大約 1 秒后會(huì)拋出錯(cuò)誤。那處理此異常的正確方法是什么?以下代碼是無效的:
function failAfterOneSecond() {
setTimeout(() => {
throw Error("Wrong!");
}, 1000);
}
try {
failAfterOneSecond();
} catch (error) {
console.error(error.message);
}
我們知道,try/catch是同步的,所以沒辦法這樣來處理異步中的錯(cuò)誤。當(dāng)傳遞給 setTimeout的回調(diào)運(yùn)行時(shí),try/catch 早已執(zhí)行完畢。程序?qū)?huì)崩潰,因?yàn)槲茨懿东@異常。它們是在兩條路徑上執(zhí)行的:
A: --> try/catch
B: --> setTimeout --> callback --> throw
我們可以監(jiān)聽頁面中任何 HTML 元素的事件,DOM 事件的錯(cuò)誤處理機(jī)制遵循與任何異步 Web API 相同的方案。
來看下面的例子:
const button = document.querySelector("button");
button.addEventListener("click", function() {
throw Error("error");
});
這里,在單擊按鈕后立即拋出了異常,我們?cè)撊绾尾东@這個(gè)異常呢?這樣寫是不起作用的,也不會(huì)阻止程序崩潰:
const button = document.querySelector("button");
try {
button.addEventListener("click", function() {
throw Error("error");
});
} catch (error) {
console.error(error.message);
}
與前面的 setTimeout 例子一樣,任何傳遞給 addEventListener 的回調(diào)都是異步執(zhí)行的:
Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw
如果不想讓程序崩潰,為了正確處理錯(cuò)誤,就必須將 try/catch 放到 addEventListener 的回調(diào)中。不過這樣做并不是最佳的處理方式,與 setTimeout 一樣,異步代碼路徑拋出的異常無法從外部捕獲,并且會(huì)使程序崩潰。
下面會(huì)介紹 Promises 和 async/await 是如何簡化異步代碼的錯(cuò)誤處理的。
HTML 元素有許多事件處理程序,例如 onclick、onmouseenter、onchange 等。除此之外,還有 onerror,每當(dāng) <img> 標(biāo)簽或 <script> 等 HTML 元素命中不存在的資源時(shí),onerror 事件處理程序就會(huì)觸發(fā)。
來看下面的例子:
<body>
<img src="nowhere-to-be-found.png">
</body>
當(dāng)訪問的資源缺失時(shí),瀏覽器的控制臺(tái)就會(huì)報(bào)錯(cuò):
GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]
在 JavaScript 中,可以使用適當(dāng)?shù)氖录幚沓绦颉安东@”此錯(cuò)誤:
const image = document.querySelector("img");
image.onerror = function(event) {
console.log(event);
};
或者使用 addEventListener 來監(jiān)聽 error 事件,當(dāng)發(fā)生錯(cuò)誤時(shí)進(jìn)行處理:
const image = document.querySelector("img");
image.addEventListener("error", function(event) {
console.log(event);
});
此模式對(duì)于加載備用資源以代替丟失的圖像或腳本很有用。不過需要記住:onerror 與 throw 或 try/catch 是無關(guān)的。
下面來通過最上面的 toUppercase 例子看看 Promise 是如何處理錯(cuò)誤的:
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Expected string");
}
return string.toUpperCase();
}
toUppercase(4);
對(duì)上面的代碼進(jìn)行修改,不返回簡單的字符串或異常,而是分別使用 Promise.reject 和 Promise.resolve 來處理錯(cuò)誤和成功:
function toUppercase(string) {
if (typeof string !== "string") {
return Promise.reject(TypeError("Expected string"));
}
const result = string.toUpperCase();
return Promise.resolve(result);
}
從技術(shù)上講,這段代碼中沒有任何異步的內(nèi)容,但它可以很好地說明 Promise 的錯(cuò)誤處理機(jī)制。
現(xiàn)在我們就可以在 then 中使用結(jié)果,并使用 catch 來處理被拒絕的 Promise:
toUppercase(99)
.then(result => result)
.catch(error => console.error(error.message));
輸出結(jié)果如下:
在 Promise 中,catch 是用來處理錯(cuò)誤的。除了 catch 還有 finally,類似于 try/catch 中的finally。不管 Promise 結(jié)果如何,finally 都會(huì)執(zhí)行:
toUppercase(99)
.then(result => result)
.catch(error => console.error(error.message))
.finally(() => console.log("Finally"));
輸出結(jié)果如下:
需要記住,任何傳遞給 then/catch/finally 的回調(diào)都是由微任務(wù)隊(duì)列異步處理的。 它們是微任務(wù),優(yōu)先于事件和計(jì)時(shí)器等宏任務(wù)。
作為拒絕 Promise 時(shí)的最佳實(shí)踐,可以傳入 error 對(duì)象:
Promise.reject(TypeError("Expected string"));
這樣,在整個(gè)代碼庫中保持錯(cuò)誤處理的一致性。 其他團(tuán)隊(duì)成員總是可以訪問 error.message,更重要的是可以檢查堆棧跟蹤。
除了 Promise.reject 之外,還可以通過拋出異常來退出 Promise 執(zhí)行鏈。來看下面的例子:
Promise.resolve("A string").then(value => {
if (typeof value === "string") {
throw TypeError("Expected number!");
}
});
這里使用 字符串來 resolve 一個(gè) Promise,然后執(zhí)行鏈立即使用 throw 斷開。為了停止異常的傳播,可以使用 catch 來捕獲錯(cuò)誤:
Promise.resolve("A string")
.then(value => {
if (typeof value === "string") {
throw TypeError("Expected number!");
}
})
.catch(reason => console.log(reason.message));
這種模式在 fetch 中很常見,可以通過檢查 response 對(duì)象來查找錯(cuò)誤:
fetch("https://example-dev/api/")
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
})
.then(json => console.log(json));
這里的異常可以使用 catch 來攔截。 如果失敗了,并且沒有攔截它,異常就會(huì)在堆棧中向上冒泡。這本身并沒有什么問題,但不同的環(huán)境對(duì)未捕獲的拒絕有不同的反應(yīng)。
例如,Node.js 會(huì)讓任何未處理 Promise 拒絕的程序崩潰:
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.
所以,最好去捕獲錯(cuò)誤。
對(duì)于計(jì)時(shí)器或事件,不能捕獲回調(diào)拋出的異常。上面有一個(gè)例子:
function failAfterOneSecond() {
setTimeout(() => {
throw Error("Error");
}, 1000);
}
// 不生效
try {
failAfterOneSecond();
} catch (error) {
console.error(error.message);
}
我們可以使用 Promise 來包裝計(jì)時(shí)器:
function failAfterOneSecond() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(Error("Error"));
}, 1000);
});
}
這里通過 reject 捕獲了一個(gè) Promise 拒絕,它帶有一個(gè) error 對(duì)象。此時(shí)就可以用 catch 來處理異常了:
failAfterOneSecond().catch(reason => console.error(reason.message));
這里使用 value 作為 Promise 的返回值,使用 reason 作為拒絕的返回對(duì)象。
Promise.all 方法接受一個(gè) Promise 數(shù)組,并返回所有解析 Promise 的結(jié)果數(shù)組:
const promise1 = Promise.resolve("one");
const promise2 = Promise.resolve("two");
Promise.all([promise1, promise2]).then((results) => console.log(results));
// 結(jié)果: ['one', 'two']
如果這些 Promise 中的任何一個(gè)被拒絕,Promise.all 將拒絕并返回第一個(gè)被拒絕的 Promise 的錯(cuò)誤。
為了在 Promise.all 中處理這些情況,可以使用 catch:
const promise1 = Promise.resolve("good");
const promise2 = Promise.reject(Error("Bad"));
const promise3 = Promise.reject(Error("Bad+"));
Promise.all([promise1, promise2, promise3])
.then(results => console.log(results))
.catch(error => console.error(error.message));
如果想要運(yùn)行一個(gè)函數(shù)而不考慮 Promise.all 的結(jié)果,可以使用 finally:
Promise.all([promise1, promise2, promise3])
.then(results => console.log(results))
.catch(error => console.error(error.message))
.finally(() => console.log("Finally"));
Promise.any 和 Promise.all 恰恰相反。Promise.all 如果某一個(gè)失敗,就會(huì)拋出第一個(gè)失敗的錯(cuò)誤。而 Promise.any 總是返回第一個(gè)成功的 Promise,無論是否發(fā)生任何拒絕。
相反,如果傳遞給 Promise.any 的所有 Promise 都被拒絕,那產(chǎn)生的錯(cuò)誤就是 AggregateError。 來看下面的例子:
const promise1 = Promise.reject(Error("Error"));
const promise2 = Promise.reject(Error("Error+"));
Promise.any([promise1, promise2])
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log("Finally"));
輸出結(jié)果如下:
這里用 catch 處理錯(cuò)誤。AggregateError 對(duì)象具有與基本錯(cuò)誤相同的屬性,外加一個(gè) errors 屬性:
const promise1 = Promise.reject(Error("Error"));
const promise2 = Promise.reject(Error("Error+"));
Promise.any([promise1, promise2])
.then(result => console.log(result))
.catch(error => console.error(error.errors))
.finally(() => console.log("Finally"));
此屬性是一個(gè)包含所有被拒絕的錯(cuò)誤信息的數(shù)組:
Promise.race 接受一個(gè) Promise 數(shù)組,并返回第一個(gè)成功的 Promise 的結(jié)果:
const promise1 = Promise.resolve("one");
const promise2 = Promise.resolve("two");
Promise.race([promise1, promise2]).then(result =>
console.log(result)
);
// 結(jié)果:one
那如果有被拒絕的 Promise,但它不是傳入數(shù)組中的第一個(gè)呢:
const promise1 = Promise.resolve("one");
const rejection = Promise.reject(Error("Bad"));
const promise2 = Promise.resolve("two");
Promise.race([promise1, rejection, promise2]).then(result =>
console.log(result)
);
// 結(jié)果:one
這樣結(jié)果還是 one,不會(huì)影響正常的執(zhí)行。
如果被拒絕的 Promise 是數(shù)組的第一個(gè)元素,則 Promise.race 拒絕,就必須要必須捕獲拒絕:
const promise1 = Promise.resolve("one");
const rejection = Promise.reject(Error("Bad"));
const promise2 = Promise.resolve("two");
Promise.race([rejection, promise1, promise2])
.then(result => console.log(result))
.catch(error => console.error(error.message));
// Bad
Promise.allSettled 是 ECMAScript 2020 新增的 API。它和 Promise.all 類似,不過不會(huì)被短路,也就是說當(dāng)Promise全部處理完成后,可以拿到每個(gè) Promise 的狀態(tài), 而不管其是否處理成功。
來看下面的例子:
const promise1 = Promise.resolve("Good!");
const promise2 = Promise.reject(Error("Bad!"));
Promise.allSettled([promise1, promise2])
.then(results => console.log(results))
.catch(error => console.error(error))
.finally(() => console.log("Finally"));
這里向 Promise.allSettled 傳遞了一個(gè)包含兩個(gè) Promise 的數(shù)組:一個(gè)已解決,另一個(gè)已拒絕。
輸出結(jié)果如下:
JavaScript 中的 async/await 表示異步函數(shù),用同步的方式去編寫異步,可讀性更好。
下面來改編上面的同步函數(shù) toUppercase,通過將 async 放在 function 關(guān)鍵字之前將其轉(zhuǎn)換為異步函數(shù):
async function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Expected string");
}
return string.toUpperCase();
}
只需在 function 前加上 async 前綴,就可以讓函數(shù)返回一個(gè) Promise。這意味著我們可以在函數(shù)調(diào)用之后鏈?zhǔn)秸{(diào)用 then、catch 和 finally:
toUppercase("hello")
.then(result => console.log(result))
.catch(error => console.error(error.message))
.finally(() => console.log("Always runs!"));
當(dāng)從 async 函數(shù)中拋出異常時(shí),異常會(huì)成為底層 Promise 被拒絕的原因。任何錯(cuò)誤都可以從外部用 catch 攔截。
除此之外,還可以使用 try/catch/finally 來處理錯(cuò)誤,就像在同步函數(shù)中一樣。
例如,從另一個(gè)函數(shù) consumer 中調(diào)用 toUppercase,它方便地用 try/catch/finally 包裝了函數(shù)調(diào)用:
async function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Expected string");
}
return string.toUpperCase();
}
async function consumer() {
try {
await toUppercase(98);
} catch (error) {
console.error(error.message);
} finally {
console.log("Finally");
}
}
consumer();
輸出結(jié)果如下:
JavaScript 中的異步生成器是能夠生成 Promise 而不是簡單值的生成器函數(shù)。它將生成器函數(shù)與異步相結(jié)合,結(jié)果是一個(gè)生成器函數(shù),其迭代器對(duì)象向消費(fèi)者公開一個(gè) Promise。
要?jiǎng)?chuàng)建一個(gè)異步生成器,需要聲明一個(gè)帶有星號(hào) * 的生成器函數(shù),前綴為 async:
async function* asyncGenerator() {
yield 33;
yield 99;
throw Error("Bad!"); // Promise.reject
}
因?yàn)楫惒缴善魇腔?Promise,所以同樣適用 Promise 的錯(cuò)誤處理規(guī)則,在異步生成器中,throw 會(huì)導(dǎo)致 Promise 拒絕,可以用 catch 攔截它。
要想從異步生成器處理 Promise,可以使用 then:
const go = asyncGenerator();
go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.next().catch(reason => console.error(reason.message));
輸出結(jié)果如下:
也使用異步迭代 for await...of。 要使用異步迭代,需要用 async 函數(shù)包裝 consumer:
async function* asyncGenerator() {
yield 33;
yield 99;
throw Error("Bad"); // Promise.reject
}
async function consumer() {
for await (const value of asyncGenerator()) {
console.log(value);
}
}
consumer();
與 async/await 一樣,可以使用 try/catch 來處理任何異常:
async function* asyncGenerator() {
yield 33;
yield 99;
throw Error("Bad"); // Promise.reject
}
async function consumer() {
try {
for await (const value of asyncGenerator()) {
console.log(value);
}
} catch (error) {
console.error(error.message);
}
}
consumer();
輸出結(jié)果如下:
從異步生成器函數(shù)返回的迭代器對(duì)象也有一個(gè) throw() 方法。在這里對(duì)迭代器對(duì)象調(diào)用 throw() 不會(huì)拋出異常,而是 Promise 拒絕:
async function* asyncGenerator() {
yield 33;
yield 99;
yield 11;
}
const go = asyncGenerator();
go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.throw(Error("Reject!"));
go.next().then(value => console.log(value));
輸出結(jié)果如下:
可以通過以下方式來捕獲錯(cuò)誤:
go.throw(Error("Let's reject!")).catch(reason =>
console.error(reason.message)
);
我們知道,迭代器對(duì)象的 throw() 是在生成器內(nèi)部發(fā)送異常的。所以還可以使用以下方式來處理錯(cuò)誤:
async function* asyncGenerator() {
try {
yield 33;
yield 99;
yield 11;
} catch (error) {
console.error(error.message);
}
}
const go = asyncGenerator();
go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.throw(Error("Reject!"));
go.next().then(value => console.log(value));
Node.js 中的同步錯(cuò)誤處理與 JavaScript 是一樣的,可以使用 try/catch/finally。
對(duì)于異步代碼,Node.js 強(qiáng)烈依賴兩個(gè)術(shù)語:
在回調(diào)模式中,異步 Node.js API 接受一個(gè)函數(shù),該函數(shù)通過事件循環(huán)處理并在調(diào)用堆棧為空時(shí)立即執(zhí)行。
來看下面的例子:
const { readFile } = require("fs");
function readDataset(path) {
readFile(path, { encoding: "utf8" }, function(error, data) {
if (error) console.error(error);
// data操作
});
}
這里可以看到回調(diào)中錯(cuò)誤處理:
function(error, data) {
if (error) console.error(error);
// data操作
}
如果使用 fs.readFile 讀取給定路徑時(shí)出現(xiàn)任何錯(cuò)誤,我們都會(huì)得到一個(gè) error 對(duì)象。這時(shí)我們可以:
要想拋出異常,可以這樣做:
const { readFile } = require("fs");
function readDataset(path) {
readFile(path, { encoding: "utf8" }, function(error, data) {
if (error) throw Error(error.message);
// data操作
});
}
但是,與 DOM 中的事件和計(jì)時(shí)器一樣,這個(gè)異常會(huì)使程序崩潰。 使用 try/catch 停止它的嘗試將不起作用:
const { readFile } = require("fs");
function readDataset(path) {
readFile(path, { encoding: "utf8" }, function(error, data) {
if (error) throw Error(error.message);
// data操作
});
}
try {
readDataset("not-here.txt");
} catch (error) {
console.error(error.message);
}
如果不想讓程序崩潰,可以將錯(cuò)誤傳遞給另一個(gè)回調(diào):
const { readFile } = require("fs");
function readDataset(path) {
readFile(path, { encoding: "utf8" }, function(error, data) {
if (error) return errorHandler(error);
// data操作
});
}
這里的 errorHandler 是一個(gè)簡單的錯(cuò)誤處理函數(shù):
function errorHandler(error) {
console.error(error.message);
// 處理錯(cuò)誤:寫入日志、發(fā)送到外部logger
}
Node.js 中的大部分工作都是基于事件的。大多數(shù)時(shí)候,我們會(huì)與發(fā)射器對(duì)象和一些偵聽消息的觀察者進(jìn)行交互。
Node.js 中的任何事件驅(qū)動(dòng)模塊(例如 net)都擴(kuò)展了一個(gè)名為 EventEmitter 的根類。EventEmitter 有兩個(gè)基本方法:on 和 emit。
下面來看一個(gè)簡單的 HTTP 服務(wù)器:
const net = require("net");
const server = net.createServer().listen(8081, "127.0.0.1");
server.on("listening", function () {
console.log("Server listening!");
});
server.on("connection", function (socket) {
console.log("Client connected!");
socket.end("Hello client!");
});
這里我們監(jiān)聽了兩個(gè)事件:listening 和 connection。除了這些事件之外,事件發(fā)射器還公開一個(gè)錯(cuò)誤事件,在出現(xiàn)錯(cuò)誤時(shí)觸發(fā)。
如果這段代碼監(jiān)聽的端口是 80,就會(huì)得到一個(gè)異常:
const net = require("net");
const server = net.createServer().listen(80, "127.0.0.1");
server.on("listening", function () {
console.log("Server listening!");
});
server.on("connection", function (socket) {
console.log("Client connected!");
socket.end("Hello client!");
});
輸出結(jié)果如下:
events.js:291
throw er;
^
Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...
為了捕獲它,可以為 error 注冊(cè)一個(gè)事件處理函數(shù):
server.on("error", function(error) {
console.error(error.message);
});
這樣就會(huì)輸出:
listen EACCES: permission denied 127.0.0.1:80
最后,我們來看看處理 JavaScript 異常的最佳實(shí)踐!
錯(cuò)處理的第一個(gè)最佳實(shí)踐就是不要過度使用“錯(cuò)誤處理”。通常,我們會(huì)在外層處理錯(cuò)誤,從內(nèi)層拋出錯(cuò)誤,這樣一旦出現(xiàn)錯(cuò)誤,就可以更好地理解是什么原因?qū)е碌摹?/span>
然而,開發(fā)人員常犯的錯(cuò)誤之一是過度使用錯(cuò)誤處理。有時(shí)這樣做是為了讓代碼在不同的文件和方法中看起來保持一致。但是,不幸的是,這些會(huì)對(duì)應(yīng)用程序和錯(cuò)誤檢測(cè)造成不利影響。
因此,只關(guān)注代碼中可能導(dǎo)致錯(cuò)誤的地方,錯(cuò)誤處理將有助于提高代碼健壯性并增加檢測(cè)到錯(cuò)誤的機(jī)會(huì)。
盡管許多瀏覽器都遵循一個(gè)通用標(biāo)準(zhǔn),但某些特定于瀏覽器的 JavaScript 實(shí)現(xiàn)在其他瀏覽器上卻失敗了。例如,以下語法僅適用于 Firefox:
catch(e) {
console.error(e.filename + ': ' + e.lineNumber);
}
因此,在處理錯(cuò)誤時(shí),盡可能使用跨瀏覽器友好的 JavaScript 代碼。
當(dāng)發(fā)生錯(cuò)誤時(shí),我們應(yīng)該得到通知以了解出了什么問題。這就是錯(cuò)誤日志的用武之地。JavaScript 代碼是在用戶的瀏覽器中執(zhí)行的。因此,需要一種機(jī)制來跟蹤客戶端瀏覽器中的這些錯(cuò)誤,并將它們發(fā)送到服務(wù)器進(jìn)行分析。
可以嘗試使用以下工具來監(jiān)控并上報(bào)錯(cuò)誤:
Node.js 環(huán)境支持使用中間件向服務(wù)端應(yīng)用中添加功能。因此可以創(chuàng)建一個(gè)錯(cuò)誤處理中間件。使用中間件的最大好處是所有錯(cuò)誤都在一個(gè)地方集中處理。可以選擇啟用/禁用此設(shè)置以輕松進(jìn)行測(cè)試。
以下是創(chuàng)建基本中間件的方法:
const logError = err => {
console.log("ERROR: " + String(err))
}
const errorLoggerMiddleware = (err, req, res, next) => {
logError(err)
next(err)
}
const returnErrorMiddleware = (err, req, res, next) => {
res.status(err.statusCode || 500)
.send(err.message)
}
module.exports = {
logError,
errorLoggerMiddleware,
returnErrorMiddleware
}
可以像下面這樣在應(yīng)用中使用此中間件:
const { errorLoggerMiddleware, returnErrorMiddleware } = require('./errorMiddleware')
app.use(errorLoggerMiddleware)
app.use(returnErrorMiddleware)
現(xiàn)在可以在中間件內(nèi)定義自定義邏輯以適當(dāng)?shù)靥幚礤e(cuò)誤。而無需再擔(dān)心在整個(gè)代碼庫中實(shí)現(xiàn)單獨(dú)的錯(cuò)誤處理結(jié)構(gòu)。
我們可能永遠(yuǎn)無法涵蓋應(yīng)用中可能發(fā)生的所有錯(cuò)誤。因此,必須實(shí)施回退策略以捕獲應(yīng)用中所有未捕獲的異常。
可以這樣做:
process.on('uncaughtException', error => {
console.log("ERROR: " + String(error))
// 其他處理機(jī)制
})
還可以確定發(fā)生的錯(cuò)誤是標(biāo)準(zhǔn)錯(cuò)誤還是自定義操作錯(cuò)誤。根據(jù)結(jié)果,可以退出進(jìn)程并重新啟動(dòng)它以避免意外行為。
與異常不同的是,promise 拒絕不會(huì)拋出錯(cuò)誤。因此,一個(gè)被拒絕的 promise 可能只是一個(gè)警告,這讓應(yīng)用有可能遇到意外行為。因此,實(shí)現(xiàn)處理 promise 拒絕的回退機(jī)制至關(guān)重要。
可以這樣做:
const promiseRejectionCallback = error => {
console.log("PROMISE REJECTED: " + String(error))
}
process.on('unhandledRejection', callback)
者|Lukas Gisder-Dubé
譯者|謝麗
本文將分三部分分析 JavaScript 中的錯(cuò)誤,首先我們將了解錯(cuò)誤的一般情況,之后,我們將關(guān)注后端(Node.js + Express.js),最后,我們將重點(diǎn)看下如何處理 React.js 中的錯(cuò)誤。選擇這些框架,是因?yàn)樗鼈兪悄壳白盍餍械模牵銘?yīng)該也能夠?qū)⑦@些新發(fā)現(xiàn)應(yīng)用到其他框架中吧!
繼上一篇文章 (https://link.medium.com/MO32x55aNR) 之后,我想談?wù)勫e(cuò)誤。錯(cuò)誤很好——我相信你以前聽過這個(gè)說法。乍一看,我們害怕錯(cuò)誤,因?yàn)殄e(cuò)誤往往會(huì)涉及到在公共場(chǎng)合受到傷害或羞辱。通過犯錯(cuò)誤,我們實(shí)際上學(xué)會(huì)了如何不去做某事,以及下次如何做得更好。
顯然,這是關(guān)于從現(xiàn)實(shí)生活的錯(cuò)誤中學(xué)習(xí)。編程中的錯(cuò)誤有點(diǎn)不同。它們?yōu)槲覀兲峁┝撕芎玫奶卣鱽砀倪M(jìn)我們的代碼,并告訴用戶什么地方出了問題(也可能是教他們?nèi)绾涡迯?fù)它)。
GitHub(https://github.com/gisderdube/graceful-error-handling) 上提供了一個(gè)完整的樣例項(xiàng)目。
throw new Error('something went wrong')?會(huì)在 JavaScript 中創(chuàng)建一個(gè)錯(cuò)誤實(shí)例,并停止腳本的執(zhí)行,除非你對(duì)錯(cuò)誤做了一些處理。當(dāng)你作為 JavaScript 開發(fā)者開啟自己的職業(yè)生涯時(shí),你自己很可能不會(huì)這樣做,但是,你已經(jīng)從其他庫(或運(yùn)行時(shí))那里看到了,例如,類似“ReferenceError: fs 未定義”這樣的錯(cuò)誤。
Error 對(duì)象
Error 對(duì)象有兩個(gè)內(nèi)置屬性供我們使用。第一個(gè)是消息,作為參數(shù)傳遞給 Error 構(gòu)造函數(shù),例如 new Error(“這是錯(cuò)誤消息”)。你可以通過 message 屬性訪問消息:
const myError = new Error(‘請(qǐng)改進(jìn)代碼’) console.log(myError.message) // 請(qǐng)改進(jìn)代碼
第二個(gè)是錯(cuò)誤堆棧跟蹤,這個(gè)屬性非常重要。你可以通過 stack 屬性訪問它。錯(cuò)誤堆棧將為你提供歷史記錄(調(diào)用堆棧),從中可以查看哪個(gè)文件導(dǎo)致了錯(cuò)誤。堆棧的上部也包括消息,然后是實(shí)際的堆棧,從距離錯(cuò)誤發(fā)生最近的點(diǎn)開始,一直到最外層“需要為錯(cuò)誤負(fù)責(zé)”的文件:
Error: 請(qǐng)改進(jìn)代碼 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)
拋出和處理錯(cuò)誤
現(xiàn)在,Error 實(shí)例本身不會(huì)導(dǎo)致任何結(jié)果,例如,new Error('...') 不會(huì)做任何事情。當(dāng)錯(cuò)誤被拋出時(shí),就會(huì)變得更有趣。然后,如前所述,腳本將停止執(zhí)行,除非你在流程中以某種方式對(duì)它進(jìn)行了處理。記住,是手動(dòng)拋出錯(cuò)誤,還是由庫拋出錯(cuò)誤,甚至由運(yùn)行時(shí)本身(Node 或?yàn)g覽器),都沒有關(guān)系。讓我們看看如何在不同的場(chǎng)景中處理這些錯(cuò)誤。
try .... catch
這是最簡單但經(jīng)常被遺忘的錯(cuò)誤處理方法——多虧 async / await,它的使用現(xiàn)在又多了起來。它可以用來捕獲任何類型的同步錯(cuò)誤,例如,如果我們不把 console.log(b) 放在一個(gè) try … catch 塊中,腳本會(huì)停止執(zhí)行。
… finally
有時(shí)候,不管是否有錯(cuò)誤,代碼都需要執(zhí)行。你可以使用第三個(gè)可選塊 finally。通常,這與在 try…catch 語句后面加一行代碼是一樣的,但它有時(shí)很有用。
異步性——回調(diào)
異步性,這是在使用 JavaScript 時(shí)必須考慮的一個(gè)主題。當(dāng)你有一個(gè)異步函數(shù),并且該函數(shù)內(nèi)部發(fā)生錯(cuò)誤時(shí),你的腳本將繼續(xù)執(zhí)行,因此,不會(huì)立即出現(xiàn)任何錯(cuò)誤。當(dāng)使用回調(diào)函數(shù)處理異步函數(shù)時(shí)(不推薦),你通常會(huì)在回調(diào)函數(shù)中收到兩個(gè)參數(shù),如下所示:
如果有錯(cuò)誤,err 參數(shù)就等同于那個(gè)錯(cuò)誤。如果沒有,參數(shù)將是 undefined 或 null。要么在 if(err) 塊中返回某項(xiàng)內(nèi)容,要么將其他指令封裝在 else 塊中,這一點(diǎn)很重要,否則你可能會(huì)得到另一個(gè)錯(cuò)誤,例如,result 可能未定義,而你試圖訪問 result.data,類似這樣的情況。
異步性——Promises
處理異步性的更好方法是使用 Promises。在這一點(diǎn)上,除了代碼可讀性更強(qiáng)之外,我們還改進(jìn)了錯(cuò)誤處理。只要有一個(gè) catch 塊,我們就不再需要太關(guān)注具體的錯(cuò)誤捕獲。在鏈接 Promises 時(shí),catch 塊捕獲會(huì)自 Promises 執(zhí)行或上一個(gè) catch 塊以來的所有錯(cuò)誤。注意,沒有 catch 塊的 Promises 不會(huì)終止腳本,但會(huì)給你一條可讀性較差的消息,比如:
(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. */
因此,務(wù)必要在 Promises 中加入 catch 塊。
回到 try … catch
隨著 JavaScript 引入 async / await,我們回到了最初的錯(cuò)誤處理方法,借助 try … catch … finally,錯(cuò)誤處理變得非常簡單。
因?yàn)檫@和我們處理“普通”同步錯(cuò)誤的方法是一樣的,所以如果需要的話,更容易使用作用域更大的 catch 語句。
現(xiàn)在,我們已經(jīng)有了處理錯(cuò)誤的工具,讓我們看下,我們?cè)趯?shí)際情況下能用它們做什么。后端錯(cuò)誤的產(chǎn)生和處理是應(yīng)用程序至關(guān)重要的組成部分。對(duì)于錯(cuò)誤處理,有不同的方法。我將向你展示一個(gè)自定義 Error 構(gòu)造函數(shù)和錯(cuò)誤代碼的方法,我們可以輕松地傳遞到前端或任何 API 消費(fèi)者。構(gòu)建后端的細(xì)節(jié)并不重要,基本思路不變。
我們將使用 Express.js 作為路由框架。讓我們考慮下最有效的錯(cuò)誤處理結(jié)構(gòu)。我們希望:
構(gòu)建一個(gè)自定義 Error 構(gòu)造函數(shù)
我們將使用已有的 Error 構(gòu)造函數(shù)并擴(kuò)展它。繼承在 JavaScript 中是一件危險(xiǎn)的事情,但根據(jù)我的經(jīng)驗(yàn),在這種情況下,它非常有用。我們?yōu)槭裁葱枰课覀內(nèi)匀幌M褩8櫧o我們一個(gè)很好的調(diào)試體驗(yàn)。擴(kuò)展 JavaScript 原生 Error 構(gòu)造函數(shù)可以讓我們方便地獲得堆棧跟蹤。我們唯一要做的是添加代碼(我們稍后可以通過錯(cuò)誤代碼訪問)和要傳遞給前端的狀態(tài)(http 狀態(tài)代碼)。
如何處理路由
在完成 Error 的自定義之后,我們需要設(shè)置路由結(jié)構(gòu)。正如我指出的那樣,我們想要一個(gè)單點(diǎn)真錯(cuò)誤處理,就是說,對(duì)于每一個(gè)路由,我們要有相同的錯(cuò)誤處理行為。在默認(rèn)情況下,由于路由都是封裝的,所以 Express 并不真正支持那種方式。
為了解決這個(gè)問題,我們可以實(shí)現(xiàn)一個(gè)路由處理程序,并把實(shí)際的路由邏輯定義為普通的函數(shù)。這樣,如果路由功能(或任何內(nèi)部函數(shù))拋出一個(gè)錯(cuò)誤,它將返回到路由處理程序,然后可以傳給前端。當(dāng)后端發(fā)生錯(cuò)誤時(shí),我們可以用以下格式傳遞一個(gè)響應(yīng)給前端——比如一個(gè) JSON API:
{ error: 'SOME_ERROR_CODE', description: 'Something bad happened. Please try again or contact support.' }
準(zhǔn)備好不知所措。當(dāng)我說下面的話時(shí),我的學(xué)生總是生我的氣:
如果你咋看之下并不是什么都懂,那沒問題。只要使用一段時(shí)間,你就會(huì)發(fā)現(xiàn)為什么要那樣。
順便說一下,這可以稱為自上而下的學(xué)習(xí),我非常喜歡。
路由處理程序就是這個(gè)樣子:
我希望你能讀下代碼中的注釋,我認(rèn)為那比我在這里解釋更有意義。現(xiàn)在,讓我們看下實(shí)際的路由文件是什么樣子:
在這些例子中,我沒有做任何有實(shí)際要求的事情,我只是假設(shè)不同的錯(cuò)誤場(chǎng)景。例如,GET /city 在第 3 行結(jié)束,POST /city 在第 8 號(hào)結(jié)束等等。這也適用于查詢參數(shù),例如,GET /city?startsWith=R。本質(zhì)上,你會(huì)有一個(gè)未處理的錯(cuò)誤,前端會(huì)收到:
{ error: 'GENERIC', description: 'Something went wrong. Please try again or contact support.' }
或者你將手動(dòng)拋出 CustomError,例如:
throw new CustomError('MY_CODE', 400, 'Error description')
上述代碼會(huì)轉(zhuǎn)換成:
{ error: 'MY_CODE', description: 'Error description' }
既然我們有了這個(gè)漂亮的后端設(shè)置,我們就不會(huì)再把錯(cuò)誤日志泄漏到前端,而總是返回有用的信息,說明出現(xiàn)了什么問題。
確保你已經(jīng)在 GitHub(https://github.com/gisderdube/graceful-error-handling) 上看過完整的庫。你可以把它用在任何項(xiàng)目中,并根據(jù)自己的需要來修改它!
下一個(gè)也是最后一個(gè)步驟是管理前端的錯(cuò)誤。這里,你要使用第一部分描述的工具處理由前端邏輯產(chǎn)生的錯(cuò)誤。不過,后端的錯(cuò)誤也要顯示。首先,讓我們看看如何顯示錯(cuò)誤。如前所述,我們將使用 React 進(jìn)行演練。
把錯(cuò)誤保存在 React 狀態(tài)中
和其他數(shù)據(jù)一樣,錯(cuò)誤和錯(cuò)誤消息會(huì)變化,因此,你想把它們放在組件狀態(tài)中。在默認(rèn)情況下,你想要在加載時(shí)重置錯(cuò)誤,以便用戶第一次看到頁面時(shí),不會(huì)看到錯(cuò)誤。
接下來我們必須澄清的是不同錯(cuò)誤類型及與其匹配的可視化表示。就像在后端一樣,有 3 種類型:
2 和 3 非常類似,雖然源頭不一樣,但如果你愿意,就可以在同樣的 state 中處理。我們將從代碼中看下如何實(shí)現(xiàn)。
我將使用 React 的原生 state 實(shí)現(xiàn),但是,你還可以使用類似 MobX 或 Redux 這樣的狀態(tài)管理系統(tǒng)。
全局錯(cuò)誤
通常,我將把這些錯(cuò)誤保存在最外層的有狀態(tài)組件中,并渲染一個(gè)靜態(tài) UI 元素,這可能是屏幕頂部的一個(gè)紅色橫幅、模態(tài)或其他什么東西,設(shè)計(jì)實(shí)現(xiàn)由你決定。
讓我們看下代碼:
正如你看到的那樣,Application.js 中的狀態(tài)存在錯(cuò)誤。我們也有方法可以重置并改變錯(cuò)誤的值。我們把值和重置方法傳遞給 GlobalError 組件,在點(diǎn)擊'x'時(shí),該組件會(huì)顯示錯(cuò)誤并重置它。讓我們看看 GlobalError 組件:
你可以看到,在第 5 行,如果沒有錯(cuò)誤,我們就不做任何渲染。這可以防止我們的頁面上出現(xiàn)一個(gè)空的紅框。當(dāng)然,你可以改變這個(gè)組件的外觀和行為。例如,你可以將“x”替換為 Timeout,幾秒鐘后重置錯(cuò)誤狀態(tài)。
現(xiàn)在,你已經(jīng)準(zhǔn)備好在任何地方使用全局錯(cuò)誤狀態(tài)了,只是從 Application.js 把 _setError 向下傳遞,而且,你可以設(shè)置全局錯(cuò)誤,例如,當(dāng)一個(gè)請(qǐng)求從后端返回了字段 error: 'GENERIC'。例如:
如果你比較懶,到這里就可以結(jié)束了。即使你有具體的錯(cuò)誤,你總是可以改變?nèi)皱e(cuò)誤狀態(tài),并把錯(cuò)誤提示框顯示在頁面頂部。不過,我將向你展示如何處理和顯示具體的錯(cuò)誤。為什么?首先,這是關(guān)于錯(cuò)誤處理的權(quán)威指南,所以我不能停在這里。其次,如果你只是把所有的錯(cuò)誤都作為全局錯(cuò)誤來顯示,那么體驗(yàn)人員會(huì)瘋掉。
處理具體的請(qǐng)求錯(cuò)誤
和全局錯(cuò)誤類似,我們也有位于其他組件內(nèi)部的局部錯(cuò)誤狀態(tài),過程相同:
有件事要記住,清除錯(cuò)誤通常有一個(gè)不同的觸發(fā)器。用' x '刪除錯(cuò)誤是沒有意義的。關(guān)于這一點(diǎn),在發(fā)出新請(qǐng)求時(shí)清除錯(cuò)誤會(huì)更有意義。你還可以在用戶進(jìn)行更改時(shí)清除錯(cuò)誤,例如當(dāng)修改輸入值時(shí)。
源于前端的錯(cuò)誤
如前所述,這些錯(cuò)誤可以使用與處理后端具體錯(cuò)誤相同的方式(狀態(tài))進(jìn)行處理。這次,我們將使用一個(gè)有輸入字段的示例,只允許用戶在實(shí)際提供以下輸入時(shí)刪除一個(gè)城市:
使用錯(cuò)誤代碼實(shí)現(xiàn)錯(cuò)誤國際化
也許你一直想知道為什么我們有這些錯(cuò)誤代碼,例如 GENERIC ,我們只是顯示從后端傳遞過來的錯(cuò)誤描述。現(xiàn)在,隨著你的應(yīng)用越來越大,你就會(huì)希望征服新的市場(chǎng),并在某個(gè)時(shí)候面臨多種語言支持的問題。如果你到了這個(gè)時(shí)候,你就可以使用前面提到的錯(cuò)誤代碼使用用戶的語言來顯示恰當(dāng)?shù)拿枋觥?/p>
我希望你對(duì)如何處理錯(cuò)誤有了一些了解。忘掉 console.error(err),它現(xiàn)在已經(jīng)是過去時(shí)了。可以使用它進(jìn)行調(diào)試,但它不應(yīng)該出現(xiàn)在最終的產(chǎn)品構(gòu)建中。為了防止這種情況,我建議你使用日志庫,我過去一直使用 loglevel,我對(duì)它非常滿意。
英文原文
https://levelup.gitconnected.com/the-definite-guide-to-handling-errors-gracefully-in-javascript-58424d9c60e6
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。