關(guān)于 JavaScript 錯誤處理的最完整指南(下半部)
使用 Promise 處理錯誤
為了演示 Promise 處理方式,我們先回到一開始的那個事例:
- function toUppercase(string) {
- if (typeof string !== "string") {
- throw TypeError("Wrong type given, expected a string");
- }
- return string.toUpperCase();
- }
- toUppercase(4);
相對簡單拋出異常,我們可以使用 Promise.reject 和Promise.resolve:
- function toUppercase(string) {
- if (typeof string !== "string") {
- return Promise.reject(TypeError("Wrong type given, expected a string"));
- }
- const result = string.toUpperCase();
- return Promise.resolve(result);
- }
因為使用了 Promise ,所以可以使用 then 來接收返回的內(nèi)容,或者用 catch 來捕獲出現(xiàn)的錯誤。
- toUppercase(99)
- .then(result => result)
- .catch(error => console.error(error.message));
上面的執(zhí)行結(jié)果:
- Wrong type given, expected a string
除了 then 和 catch , Promise 中還有 finally 方法,這類似于try/catch 中的 finally。
- toUppercase(99)
- .then(result => result)
- .catch(error => console.error(error.message))
- .finally(() => console.log("Run baby, run"));
Promise, error, 和 throw
使用 Promise.reject 可以很方便的拋出錯誤:
- Promise.reject(TypeError("Wrong type given, expected a string"));
除了Promise.reject,我們也可以通過拋出異常來退出 Promise。
考慮以下示例:
- Promise.resolve("A string").then(value => {
- if (typeof value === "string") {
- throw TypeError("Expected a number!");
- }
- });
要停止異常傳播,我們照常使用catch:
- Promise.resolve("A string")
- .then(value => {
- if (typeof value === "string") {
- throw TypeError("Expected a number!");
- }
- })
- .catch(reason => console.log(reason.message));
這種模式在fetch中很常見:
- fetch("https://example-dev/api/")
- .then(response => {
- if (!response.ok) {
- throw Error(response.statusText);
- }
- return response.json();
- })
- .then(json => console.log(json));
這里可以使用catch攔截異常。如果我們失敗了,或者決定不捕獲它,異??梢栽诙褩V凶杂擅芭?。
使用 Promise 來處理定時器中的異常
使用定時器或事件無法捕獲從回調(diào)引發(fā)的異常。
- function failAfterOneSecond() {
- setTimeout(() => {
- throw Error("Something went wrong!");
- }, 1000);
- }
- // DOES NOT WORK
- try {
- failAfterOneSecond();
- } catch (error) {
- console.error(error.message);
- }
解決方案就是使用 Promise:
- function failAfterOneSecond() {
- return new Promise((_, reject) => {
- setTimeout(() => {
- reject(Error("Something went wrong!"));
- }, 1000);
- });
- }
使用reject,我們啟動了一個 Promise 拒絕,它攜帶一個錯誤對象。
此時,我們可以使用catch處理異常:
- failAfterOneSecond().catch(reason => console.error(reason.message));
使用 Promise.all 來處理錯誤
Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數(shù)內(nèi)所有的 promise 都“完成(resolved)”或參數(shù)中不包含 promise 時回調(diào)完成(resolve);
- const promise1 = Promise.resolve("All good!");
- const promise2 = Promise.resolve("All good here too!");
- Promise.all([promise1, promise2]).then((results) => console.log(results));
- // [ 'All good!', 'All good here too!' ]
如果參數(shù)中 promise 有一個失敗(rejected),此實例回調(diào)失敗(reject),失敗的原因是第一個失敗 promise 的結(jié)果。
- const promise1 = Promise.resolve("All good!");
- const promise2 = Promise.reject(Error("No good, sorry!"));
- const promise3 = Promise.reject(Error("Bad day ..."));
- Promise.all([promise1, promise2, promise3])
- .then(results => console.log(results))
- .catch(error => console.error(error.message));
- // No good, sorry!
同樣,無論Promise.all的結(jié)果如何運行函數(shù),finally 都會被執(zhí)行:
- Promise.all([promise1, promise2, promise3])
- .then(results => console.log(results))
- .catch(error => console.error(error.message))
- .finally(() => console.log("Always runs!"));
使用 Promise.any 來處理錯誤
Promise.any() (Firefox > 79, Chrome > 85) 接收一個 Promise 可迭代對象,只要其中的一個 promise 成功,就返回那個已經(jīng)成功的 promise 。如果可迭代對象中沒有一個 promise 成功(即所有的 promises 都失敗/拒絕),就返回一個失敗的 promise 和AggregateError類型的實例,它是 Error 的一個子類,用于把單一的錯誤集合在一起。本質(zhì)上,這個方法和Promise.all()是相反的。
- const promise1 = Promise.reject(Error("No good, sorry!"));
- const promise2 = Promise.reject(Error("Bad day ..."));
- Promise.any([promise1, promise2])
- .then(result => console.log(result))
- .catch(error => console.error(error))
- .finally(() => console.log("Always runs!"));
在這里,我們使用catch處理錯誤,輸出如下:
- AggregateError: No Promise in Promise.any was resolved
- Always runs!
AggregateError對象具有與基本Error相同的屬性,外加errors屬性:
- //
- .catch(error => console.error(error.errors))
- //
此屬性是由reject產(chǎn)生的每個單獨錯誤的數(shù)組
- [Error: "No good, sorry!, Error: "Bad day ..."]
使用 Promise.race 來處理錯誤
Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。
- const promise1 = Promise.resolve("The first!");
- const promise2 = Promise.resolve("The second!");
- Promise.race([promise1, promise2]).then(result => console.log(result));
- // The first!
這里說明,第一個 Promise 比第二個行執(zhí)行完。那包含拒絕的情況又是怎么樣的?
- const promise1 = Promise.resolve("The first!");
- const rejection = Promise.reject(Error("Ouch!"));
- const promise2 = Promise.resolve("The second!");
- Promise.race([promise1, rejection, promise2]).then(result =>
- console.log(result)
- );
- // The first!
如果把reject放在第一個又會怎么樣?
- const promise1 = Promise.resolve("The first!");
- const rejection = Promise.reject(Error("Ouch!"));
- const promise2 = Promise.resolve("The second!");
- Promise.race([rejection, promise1, promise2])
- .then(result => console.log(result))
- .catch(error => console.error(error.message));
- // Ouch!
使用 Promise.allSettled 來處理錯誤
Promise.allSettled()方法返回一個在所有給定的promise都已經(jīng)fulfilled或rejected后的promise,并帶有一個對象數(shù)組,每個對象表示對應(yīng)的promise結(jié)果。
考慮下面示例:
- const promise1 = Promise.resolve("Good!");
- const promise2 = Promise.reject(Error("No good, sorry!"));
- Promise.allSettled([promise1, promise2])
- .then(results => console.log(results))
- .catch(error => console.error(error))
- .finally(() => console.log("Always runs!"));
我們傳遞給Promise.allSettled一個由兩個Promise組成的數(shù)組:一個已解決,另一個被拒絕。
這種情況 catch 不會被執(zhí)行, finally 永遠會執(zhí)行。
- [
- { status: 'fulfilled', value: 'Good!' },
- {
- status: 'rejected',
- reason: Error: No good, sorry!
- }
- ]
使用 async/await 來處理錯誤
為了簡單起見,我們使用前面的同步函數(shù)toUppercase,并通過在function關(guān)鍵字前放置async來將其轉(zhuǎn)換為異步函數(shù)
- async function toUppercase(string) {
- if (typeof string !== "string") {
- throw TypeError("Wrong type given, expected a string");
- }
- return string.toUpperCase();
- }
只要在函數(shù)前面加上async,該函數(shù)就會返回一個Promise。這意味著我們可以在函數(shù)調(diào)用之后進行then、catch和finally 操作
- async function toUppercase(string) {
- if (typeof string !== "string") {
- throw TypeError("Wrong type given, expected a string");
- }
- return string.toUpperCase();
- }
- toUppercase("abc")
- .then(result => console.log(result))
- .catch(error => console.error(error.message))
- .finally(() => console.log("Always runs!"));
當從 async 函數(shù)拋出異常時,我們就可以使用 catch 來捕獲。
最重要的是,除了這種方式外,我們可以還使用try/catch/finally,就像我們使用同步函數(shù)所做的一樣。
- async function toUppercase(string) {
- if (typeof string !== "string") {
- throw TypeError("Wrong type given, expected a string");
- }
- return string.toUpperCase();
- }
- async function consumer() {
- try {
- await toUppercase(98);
- } catch (error) {
- console.error(error.message);
- } finally {
- console.log("Always runs!");
- }
- }
- consumer();
輸出:
- Wrong type given, expected a string
- Always runs!
使用 async generators 來處理錯誤
JavaScript中的async generators是能夠生成 Promises 而不是簡單值的生成器函數(shù)。
- async function* asyncGenerator() {
- yield 33;
- yield 99;
- throw Error("Something went wrong!"); // Promise.reject
- }
基于 Promise,此處適用于錯誤處理的相同規(guī)則。在異步生成器中 throw 將會觸發(fā) Promise 的reject,我們可以使用catch對其進行攔截。
為了使用異步生成器的 Promise,我們可以這樣做:
- then 方法
- 異步遍歷
從上面我們知道,在兩次調(diào)用 yield之后,下一次會拋出一個異常:
- 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é)果:
- { value: 33, done: false }
- { value: 99, done: false }
- Something went wrong!
別一種是使用 異步遍歷與for await...of:
- async function* asyncGenerator() {
- yield 33;
- yield 99;
- throw Error("Something went wrong!"); // 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("Something went wrong!"); // Promise.reject
- }
- async function consumer() {
- try {
- for await (const value of asyncGenerator()) {
- console.log(value);
- }
- } catch (error) {
- console.error(error.message);
- }
- }
- consumer();
輸出結(jié)果:
- 33
- 99
- Something went wrong!
從異步生成器函數(shù)返回的迭代器對象也具有throw()方法,非常類似于其同步副本。在此處的迭代器對象上調(diào)用throw()不會引發(fā)異常,但是會被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("Let's reject!"));
- go.next().then(value => console.log(value)); // value is undefined
要從外部處理這種情況,我們可以做:
- go.throw(Error("Let's reject!")).catch(reason => console.error(reason.message));
Node 中的錯誤處理
Node 中的同步錯誤處理
Node.js 中的同步錯誤處理與到目前為止所看到的并沒有太大差異。對于同步,使用 try/catch/finally 就可以很好的工作了。
Node.js 中的異步錯誤處理:回調(diào)模式
對于異步代碼,Node.js 主要使用這兩種方式:
- 回調(diào)模式
- event emitters
在回調(diào)模式中,異步 Node.js API 接受一個函數(shù),該函數(shù)通過事件循環(huán)處理,并在調(diào)用堆棧為空時立即執(zhí)行。
考慮以下代碼:
- const { readFile } = require("fs");
- function readDataset(path) {
- readFile(path, { encoding: "utf8" }, function(error, data) {
- if (error) console.error(error);
- // do stuff with the data
- });
- }
我們可以看到,這里處理錯誤的方式是使用了回調(diào):
- //
- function(error, data) {
- if (error) console.error(error);
- // do stuff with the data
- }
- //
如果使用fs.readFile讀取給定路徑而引起任何錯誤,我們將獲得一個錯誤對象。
在這一點上,我們可以:
- 簡單的把對象錯誤打出來
- 拋出錯誤
- 把錯誤傳到另一個回調(diào)
我們可以拋出一個異常
- const { readFile } = require("fs");
- function readDataset(path) {
- readFile(path, { encoding: "utf8" }, function(error, data) {
- if (error) throw Error(error.message);
- // do stuff with the data
- });
- }
但是,與 DOM 中的事件和定時器一樣,此異常將使程序崩潰。通過try/catch捕獲它是不起作用的:
- const { readFile } = require("fs");
- function readDataset(path) {
- readFile(path, { encoding: "utf8" }, function(error, data) {
- if (error) throw Error(error.message);
- // do stuff with the data
- });
- }
- try {
- readDataset("not-here.txt");
- } catch (error) {
- console.error(error.message);
- }
如果我們不想使程序崩潰,則將錯誤傳遞給另一個回調(diào)是首選方法:
- const { readFile } = require("fs");
- function readDataset(path) {
- readFile(path, { encoding: "utf8" }, function(error, data) {
- if (error) return errorHandler(error);
- // do stuff with the data
- });
- }
這里的errorHandler顧名思義,是一個用于錯誤處理的簡單函數(shù):
- function errorHandler(error) {
- console.error(error.message);
- // do something with the error:
- // - write to a log.
- // - send to an external logger.
- }
Node.js 中的異步錯誤處理:event emitters
在 Node.js 中所做的大部分工作都是基于事件的。大多數(shù)情況下,emitter object 和一些觀察者進行交互以偵聽消息。
Node.js中的任何事件驅(qū)動模塊(例如net)都擴展了一個名為EventEmitter的根類。
Node.js中的EventEmitter有兩種基本方法:on和emit。
考慮以下簡單的 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!");
- });
這里我們來聽兩個事件:listening 和connection。除了這些事件之外,event emitters 還公開一個 error 事件,以防發(fā)生錯誤。
如果在端口80上運行這段代碼,而不是在前面的示例上偵聽,將會得到一個異常:
- 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!");
- });
輸出:
- events.js:291
- throw er; // Unhandled 'error' event
- ^
- Error: listen EACCES: permission denied 127.0.0.1:80
- Emitted 'error' event on Server instance at: ...
要捕獲它,我們可以注冊一個error事件處理程序:
- server.on("error", function(error) {
- console.error(error.message);
- });
輸出結(jié)果:
- listen EACCES: permission denied 127.0.0.1:80
總結(jié)
在這個指南中,我們介紹了JavaScript的各種錯誤處理,從簡單的同步代碼到高級的異步。在JavaScript程序中,可以通過多種方式來捕獲異常。
同步代碼中的異常是最容易捕獲的。相反,異步中的異常需要一些技巧來處理。
瀏覽器中的新JavaScript API幾乎都偏向 Promise。then/catch/finally或try/catch的模式對于async/await的異常處理變得更加容易。
作者:Valentino Gagliardi 譯者:前端小智 來源:valentinog
原文:https://www.valentinog.com/blog/error/
本文轉(zhuǎn)載自微信公眾號「 大遷世界」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 大遷世界公眾號。