本文最初发布于 valentinog.com 网站,经原作者授权由 InfoQ 中文站翻译并分享。
什么是编程中的错误?
在我们的程序中,事物并非总是一帆风顺的。
特别是在某些情况下,我们可能希望 停止程序或在发生意外错误时通知用户。
例如:
程序试图打开一个不存在的文件。
网络连接断开。
用户输入了无效的内容。
在所有这些情况下,我们程序员都会创建 错误,或者让编程引擎为我们创建一些错误。
在创建错误之后,我们可以向用户发送一条消息,或者完全停止执行。
JavaScript 中有什么错误?
JavaScript 中的一个错误是一个对象,错误会被 抛出 以暂停程序。
要在 JavaScript 中创建一个新错误,我们需要调用适当的 构造函数。例如,要创建一个新的泛型错误,我们可以执行以下操作:
const err = new Error("Something bad happened!");
复制代码
创建一个错误对象时,也可以省略 new 关键字:
const err = Error("Something bad happened!");
复制代码
创建后,错误对象将显示三个属性:
message:包含错误消息的字符串。
name:错误的类型。
stack:函数执行的堆栈跟踪。
例如,如果我们创建一个新的 TypeError 对象,带有适当的消息,该 message 将携带实际的错误字符串,而 name 将为“TypeError”:
const wrongType = TypeError("Wrong type given, expected number");
wrongType.message; // "Wrong type given, expected number"
wrongType.name; // "TypeError"
复制代码
Firefox 还实现了一些非标准属性,如 columnNumber、filename 和 lineNumber。
JavaScript 中的错误类型
JavaScript 中有很多错误类型,包括:
Error
EvalError
InternalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
请记住,所有这些错误类型都是 实际的构造函数,旨在返回一个新的错误对象。
在代码中,你将主要使用 Error 和 TypeError 这两种最常见的类型来创建自己的错误对象。
一般来说,大多数错误将直接来自 JavaScript 引擎,例如 InternalError 或 SyntaxError。
当你尝试重赋值 const 时,会发生 TypeError:
const name = "Jules";
name = "Caty";
// TypeError: Assignment to constant variable.
复制代码
当你的语言关键字拼写错误时,会发生 SyntaxError:
va x = '33';
// SyntaxError: Unexpected identifier
复制代码
或者,当你在错误的地方使用保留的关键字时,例如在一个 async 函数外部 await:
function wrong(){
await 99;
}
wrong();
// SyntaxError: await is only valid in async function
复制代码
当我们在页面中选择不存在的 HTML 元素时,也会发生 TypeError:
Uncaught TypeError: button is null
复制代码
除了这些传统的错误对象外,JavaScript 中很快还会有 AggregateError 对象。AggregateError 可以很容易地将多个错误包装在一起,后文会具体介绍。
除了这些内置错误外,在浏览器中我们还可以找到:
DOMException。
DOMError,已弃用,如今不再使用。
DOMException 是与 WebAPI 相关的一系列错误。当我们在浏览器中做蠢事时它们就会被抛出,例如:
document.body.appendChild(document.cloneNode(true));
复制代码
结果:
Uncaught DOMException: Node.appendChild: May not add a Document as a child
复制代码
有关完整列表,请参见 MDN 上的这一页面。
什么是异常?
多数开发人员认为错误和异常是同一回事。实际上,一个错误对象只有在被抛出时才成为异常。
要在 JavaScript 中抛出一个异常,我们使用 throw,然后是错误对象:
const wrongType = TypeError("Wrong type given, expected number");
throw wrongType;
复制代码
缩写形式更常见,在大多数代码库中你都可以找到:
throw TypeError("Wrong type given, expected number");
复制代码
或:
throw new TypeError("Wrong type given, expected number");
复制代码
不太可能将异常抛出到函数或条件块之外。相反,考虑以下示例:
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Wrong type given, expected a string");
}
return string.toUpperCase();
}
复制代码
在这里,我们检查这个函数参数是否为一个字符串。如果不是,我们抛出一个异常。从技术上讲,你可以在 JavaScript 中抛出任何内容,而不仅仅是错误对象:
throw Symbol();
throw 33;
throw "Error!";
throw null;
复制代码
但最好避免这些事情,始终抛出正确的错误对象,而不是基元。这样,你就可以在代码库中保持错误处理的一致性。其他团队成员就能一直在错误对象上访问 error.message 或 error.stack。
当我们抛出异常时会发生什么?
异常就像在上升的电梯:一旦抛出一个,它就会在程序栈中冒泡,除非它在某个地方被捕获。
考虑以下代码:
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Wrong type given, expected a string");
}
return string.toUpperCase();
}
toUppercase(4);
复制代码
如果你在浏览器或 Node.js 中运行此代码,程序将停止并报告错误:
Uncaught TypeError: Wrong type given, expected a string
toUppercase http://localhost:5000/index.js:3
<anonymous> http://localhost:5000/index.js:9
复制代码
此外,你可以看到发生错误的具体代码行。这个报告是一个 堆栈跟踪,对于跟踪代码中的问题很有帮助。
堆栈跟踪的顺序是从底到顶的。所以在这里:
toUppercase http://localhost:5000/index.js:3
<anonymous> http://localhost:5000/index.js:9
复制代码
我们可以说:
第 9 行中的代码调用了 toUppercase
toUppercase 在第 3 行爆炸了
除了在浏览器的控制台中看到这个堆栈跟踪外,你还可以在错误对象的 stack 属性上访问它。
如果这个异常 未捕获,即程序员没有采取任何措施来捕获它,则程序将崩溃。
在何时何地捕获代码中的异常取决于具体的用例。
例如,你可能想在堆栈中传播一个异常,以使程序完全崩溃。出现致命的错误时可能就会是这种情况,因为停止程序比处理无效数据更安全。
介绍了基础知识之后,现在我们来研究 同步和异步 JavaScript 代码中的错误和异常处理。
同步错误处理
同步代码在大多数情况下很简单,它的错误处理也是如此。
常规函数的错误处理 同步代码的执行顺序和代码的编写顺序一致。再来看前面的示例:
function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Wrong type given, expected a string");
}
return string.toUpperCase();
}
toUppercase(4);
复制代码
在这里,引擎调用并执行 toUppercase。所有这些都是 同步 发生的。要 捕获 由此类同步函数引发的异常,我们可以使用 try/catch/finally:
try {
toUppercase(4);
} catch (error) {
console.error(error.message);
// or log remotely
} finally {
// clean up
}
复制代码
通常,try 处理最简单的场景,或可能抛出错误的函数调用。catch 则会 捕获实际的异常。它 接收错误对象,我们可以检查该错误对象(并将其远程发送到生产环境中的某些记录器)。
另一方面,无论函数的结果如何,finally 语句都会运行:无论是失败还是成功,final 内部的任何代码都将运行。
记住:try/catch/finally 是一个 同步 结构:它现在具有捕获来自异步代码异常的方法。
生成器函数的错误处理
JavaScript 中的生成器(generator)函数是一种特殊的函数。
除了在其内部作用域和消费者之间提供 双向通信通道 外,它可以 随意暂停和恢复。
要创建一个生成器函数,我们在 function 关键字后加一个星号 *:
function* generate() {
//
}
复制代码
一旦进入函数,我们就可以使用 yield 来返回值:
function* generate() {
yield 33;
yield 99;
}
复制代码
生成器函数的返回值 是 迭代器(iterator)对象。为了 从生成器中提取值,我们可以使用两种方法:
在迭代器对象上调用 next()。
for…of 的 迭代。
以我们的示例为例,要从生成器获取值,我们可以这样做:
function* generate() {
yield 33;
yield 99;
}
const go = generate();
复制代码
当我们调用生成器函数时,go 成为我们的迭代器对象。从现在开始,我们可以调用 go.next() 来推进执行:
function* generate() {
yield 33;
yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99
复制代码
生成器也有另一种工作机制:它们可以接受调用者返回的值和异常。除了 next() 之外,从生成器返回的迭代器对象还具有 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
复制代码
要捕获此类错误,你可以使用 try/catch 将代码包装在生成器中(如果需要的话也可以用 finally):
function* generate() {
try {
yield 33;
yield 99;
} catch (error) {
console.error(error.message);
}
}
复制代码
生成器函数还可以向外部抛出异常。捕获这些异常的机制与捕获同步异常的机制相同:try/catch/finally。
这是一个从外部使用 for…of 消费的生成器函数的示例:
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);
}
/* Output:
33
99
Tired of iterating!
*/
复制代码
在这里,我们迭代 try 块中的 happy path。如果发生任何异常,我们将使用 catch 停止它。
异步错误处理
JavaScript 本质上是同步的,是一种单线程语言。
浏览器引擎之类的主机环境使用许多 WebAPI 增强了 JavaScript,以同外部系统交互并处理 I/O 相关联的操作。
浏览器中的异步性示例包括超时、事件和 Promise。
异步世界中的错误处理 与同步世界是不一样的。
我们来看一些例子。
计时器错误处理
开始探索 JavaScript 时,在学习了 try/catch/finally 之后,你可能会想将它放在任何代码块中。
考虑以下代码段:
function failAfterOneSecond() {
setTimeout(() => {
throw Error("Something went wrong!");
}, 1000);
}
复制代码
此函数将在大约 1 秒钟后抛出错误。处理此异常的正确方法是什么?以下示例 不起作用:
function failAfterOneSecond() {
setTimeout(() => {
throw Error("Something went wrong!");
}, 1000);
}
try {
failAfterOneSecond();
} catch (error) {
console.error(error.message);
}
复制代码
正如我们所说,try/catch 是同步的。另一方面,我们有 setTimeout,这是一个用于计时器(timer)的浏览器 API。到传递给 setTimeout 的回调运行时,我们的 try/catch 早就没了。该程序将崩溃,因为我们无法捕获异常。
它们走的是两条不同的路径:
Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw
复制代码
如果我们不想让程序崩溃,为了正确处理错误,我们必须在 setTimeout 的回调内移动 try/catch。但是,这种方法在大多数情况下没有多大意义。稍后我们将看到,使用 Promises 进行异步错误处理可提供更好的开发体验。
事件错误处理
文档对象模型(DOM)中的 HTML 节点连接到 EventTarget,EventTarget 是浏览器中任何事件发射器(emitter)的公共祖先。
这意味着我们可以侦听页面中任何 HTML 元素上的事件:
https://www.valentinog.com/blog/event/#how-does-event-driven-applies-to-javascript-in-the-browser
(Node.js 会在未来版本中支持 EventTarget)。
DOM 事件的错误处理机制遵循异步 WebAPI 的模式。
考虑以下示例:
const button = document.querySelector("button");
button.addEventListener("click", function() {
throw Error("Can't touch this button!");
});
复制代码
在这里,单击按钮后立即抛出一个异常。我们如何捕获它呢?这个模式 不起作用,也不会阻止程序崩溃:
const button = document.querySelector("button");
try {
button.addEventListener("click", function() {
throw Error("Can't touch this button!");
});
} catch (error) {
console.error(error.message);
}
复制代码
与前面带有 setTimeout 的示例一样,传递给 addEventListener 的任何回调均 异步 执行:
Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw
复制代码
如果我们不希望程序崩溃,则要正确处理错误,我们必须在 addEventListener 的回调中移动 try/catch。但同样,这样做几乎没有任何价值。
与 setTimeout 一样,异步代码路径抛出的异常 无法从外部捕获,这将使程序崩溃。
在下一部分中,我们将了解如何使用 Promises 和 async/await 简化异步代码的错误处理。
onerror 是什么情况?
HTML 元素有许多事件处理器,例如 onclick、onmouseenter、onchange 等。
还有 onerror,但它与 throw 之类是无关的。
每当<img>
标签或<script>
之类的 HTML 元素遇到不存在的资源时,onerror 事件处理器都会触发。
考虑以下示例:
// omitted
<body>
<img src="nowhere-to-be-found.png" alt="So empty!">
</body>
// omitted
复制代码
当访问缺少资源或不存在资源的 HTML 文档时,浏览器的控制台会记录以下错误:
GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]
复制代码
在 JavaScript 中,我们可以使用适当的事件处理器来“捕获”此错误:
const image = document.querySelector("img");
image.onerror = function(event) {
console.log(event);
};
复制代码
更好的是:
const image = document.querySelector("img");
image.addEventListener("error", function(event) {
console.log(event);
});
复制代码
此模式可以方便地 加载替代资源来代替丢失的图像或脚本。但是请记住:onerror 与 throw 或 try/catch 无关。
使用 Promise 处理错误
为了说明用 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 来消费结果,并 catch 以 处理被拒绝的 Promise:
toUppercase(99)
.then(result => result)
.catch(error => console.error(error.message));
复制代码
代码会记录:
Wrong type given, expected a string
复制代码
在 Promise 的世界中,catch 是用于处理错误的结构。除了 catch 和 then,我们也有 finally,类似于 try/catch 中的 finally。
作为其同步的“相对”,Promise 的 finally 无论 Promise 的结果如何都会运行:
toUppercase(99)
.then(result => result)
.catch(error => console.error(error.message))
.finally(() => console.log("Run baby, run"));
复制代码
谨记,传递给 then/catch/finally 的任何回调都是由微任务队列异步处理的。它们是 微任务,优先于事件和计时器等宏任务。
Promise,错误和抛出
作为 拒绝 Promise 的最佳实践,提供错误对象很方便:
Promise.reject(TypeError("Wrong type given, expected a string"));
复制代码
这样,你可以在代码库中保持错误处理的一致性。其他团队成员总是能访问 error.message,更重要的是你可以检查堆栈跟踪。除了 Promise.reject,我们还可以通过 抛出 异常来退出 Promise 链。
考虑以下示例:
Promise.resolve("A string").then(value => {
if (typeof value === "string") {
throw TypeError("Expected a number!");
}
});
复制代码
我们用一个字符串解析一个 Promise,然后 Promise 链会立刻被 throw 断开。要停止异常传播,我们照常使用 catch:
Promise.resolve("A string")
.then(value => {
if (typeof value === "string") {
throw TypeError("Expected a number!");
}
})
.catch(reason => console.log(reason.message));
复制代码
这种模式在 fetch 中很常见,我们在 fetch 中检查响应对象以查找错误:
fetch("https://example-dev/api/")
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
})
.then(json => console.log(json));
复制代码
在这里,异常可以被 catch 拦截。如果我们失败了,或者决定不在这里捕获它,那么 异常就可以在堆栈中冒泡了。这本身并不坏,但是不同的环境对未捕获的拒绝的反应是不同的。
例如,将来的 Node.js 将使任何未处理 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.
复制代码
所以最好捕获它们!
“Promise 化”计时器的错误处理
使用计时器或事件无法捕获从回调抛出的异常。我们在上一节中看到了一个示例:
function failAfterOneSecond() {
setTimeout(() => {
throw Error("Something went wrong!");
}, 1000);
}
// DOES NOT WORK
try {
failAfterOneSecond();
} catch (error) {
console.error(error.message);
}
复制代码
Promise 提供的一个解决方案是代码的“Promise 化”。具体来说,我们用 Promise 包装计时器:
function failAfterOneSecond() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(Error("Something went wrong!"));
}, 1000);
});
}
复制代码
通过 reject,我们启动了一个 Promise 拒绝,带有一个错误对象。这时,我们可以使用 catch 处理异常:
failAfterOneSecond().catch(reason => console.error(reason.message));
复制代码
注意:通常使用 value 作为 Promise 的返回值,并使用 reason 作为拒绝的返回对象。
Node.js 有一个名为 promisify 的实用程序,可简化旧式回调 API 的“Promise 化”。
https://nodejs.org/api/util.html#util_util_promisify_original
Promise.all 中的错误处理
静态方法 Promise.all 接收一个 Promise 数组,并从所有解析中的 Promise 返回一个结果数组:
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!' ]
复制代码
如果这些 Promise 中的任何一个被拒绝,Promise.all 都会拒绝,并返回第一个被拒绝的 Promise 中的错误。为了在 Promise.all 中处理这些情况,我们像上一节中一样使用 catch:
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));
复制代码
同样,无论 Promise.all 的结果如何都要运行函数的话,我们可以使用 finally:
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.all 的反面。
即使数组中只有一个 Promise 拒绝,Promise.all 也会返回失败;而 Promise.any 始终为我们提供第一个已解析的 Promise(如果存在于数组中),不管发生了什么拒绝。
如果 所有 传递给 Promise.any 的 Promise 都拒绝,则产生的错误是 AggregateError。考虑以下示例:
Promise.all([promise1, promise2, promise3])
.then(results => console.log(results))
.catch(error => console.error(error.message))
.finally(() => console.log("Always runs!"));
复制代码
在这里,我们使用 catch 处理错误。此代码的输出是:
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!"));
复制代码
AggregateError 对象具有与基础的 Error 相同的属性,外加一个 errors 属性:
AggregateError: No Promise in Promise.any was resolved
Always runs!
复制代码
此属性是由拒绝产生的各个错误组成的数组:
//
.catch(error => console.error(error.errors))
//
复制代码
Promise.race 中的错误处理
静态方法 Promise.race 接收一个 Promise 数组:
[Error: "No good, sorry!, Error: "Bad day ..."]
复制代码
结果是 第一个赢得“比赛”的 Promise。那拒绝呢?如果拒绝的 Promise 不是第一个出现在输入数组中的对象,则 Promise.race 解析:
const promise1 = Promise.resolve("The first!");
const promise2 = Promise.resolve("The second!");
Promise.race([promise1, promise2]).then(result => console.log(result));
// The first!
复制代码
如果 拒绝出现在数组的第一个元素中,则 Promise.race 拒绝,且我们必须捕获这个拒绝:
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 是 ECMAScript 2020 加入的。
使用这种静态方法没有什么要处理的,因为 即使一个或多个输入 Promise 拒绝,结果始终是一个已解析的 Promise。
考虑以下示例:
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 组成的数组:一个已解析,另一个被拒绝。在这种情况下,catch 将永远不会启用。于是会运行 finally。
代码的结果记录在 then 中,如下:
[
{ status: 'fulfilled', value: 'Good!' },
{
status: 'rejected',
reason: Error: No good, sorry!
}
]
复制代码
async/await 的错误处理
JavaScript 中的 async/await 表示异步函数,但是从读者的角度来看,它们也拥有同步函数的所有 可读性。
为简单起见,我们将先前的同步函数设为 Uppercase,并在 function 关键字之前放置 async,以将其转换为异步函数:
async function toUppercase(string) {
if (typeof string !== "string") {
throw TypeError("Wrong type given, expected a string");
}
return string.toUpperCase();
}
复制代码
只需在函数前面加上 async 前缀,我们就可以使函数 返回一个 Promise。这意味着我们可以在函数调用之后来一串 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 函数中抛出异常时,异常将成为底层 Promise 被拒绝的原因。
可以使用 catch 从外部拦截任何错误。
最重要的是,除了这种样式外,我们还可以使用 try/catch/finally,就像我们使用同步函数时所做的一样。
在下面的示例中,我们从另一个函数 consumer 调用 toUppercase,前者方便地用 try/catch/finally 将函数调用包装起来:
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(); // Returning Promise ignored
复制代码
输出是:
Wrong type given, expected a string
Always runs!
复制代码
同一主题的资料:如何从 JavaScript 中的 async 函数抛出错误?
https://www.valentinog.com/blog/throw-async/
异步生成器的错误处理
JavaScript 中的 异步生成器 是 能够生成 Promise 而非简单值的生成器函数。
它们将生成器函数与 async 结合在一起。结果是一个生成器函数,其迭代器对象将一个 Promise 暴露给消费者。
要创建一个异步生成器,我们用星号 * 声明一个生成器函数,加一个 async 前缀:
async function* asyncGenerator() {
yield 33;
yield 99;
throw Error("Something went wrong!"); // Promise.reject
}
复制代码
此处的错误处理规则也是和 Promise 一样的。在异步生成器中 throw 将导致一个 Promise 拒绝,我们使用 catch 拦截它。要从异步生成器拉出 Promise,我们可以使用两种方法:
从上面的示例中,我们可以肯定地知道在前两个 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));
复制代码
代码输出是:
{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!
复制代码
另一种方法是使用 for await…of 的 async 迭代。要使用 async 迭代,我们需要使用一个 async 函数包装这个消费者。
下面是完整的示例:
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();
复制代码
代码输出是:
33
99
Something went wrong!
复制代码
从异步生成器函数返回的迭代器对象也有一个 throw() 方法,非常像它的同步形式。在此处的迭代器对象上调用 throw() 不会抛出异常,而是一个 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));
复制代码
但是,请不要忘记迭代器对象 throw()在生成器内部 发送异常。也就是说我们还可以应用以下模式:
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("Let's reject!"));
go.next().then(value => console.log(value)); // value is undefined
复制代码
Node.js 中的错误处理
Node.js 中的同步错误处理
Node.js 中的同步错误处理与上文介绍的内容并没有太大差异。
对于 同步代码,try/catch/finally 没什么问题。
但如果我们进入异步世界,事情就会变得很有趣了。
Node.js 中的异步错误处理:回调模式
对于异步代码,Node.js 强烈依赖两个习惯用法:
在 回调模式 中,异步 Node.jsAPI 接收一个函数,该函数通过 事件循环 处理,并在 调用堆栈 为空时立即执行。
考虑以下代码:
const { readFile } = require("fs");
function readDataset(path) {
readFile(path, { encoding: "utf8" }, function(error, data) {
if (error) console.error(error);
// do stuff with the data
});
}
复制代码
如果从此清单中提取回调,就可以看到错误的处理方式:
//
function(error, data) {
if (error) console.error(error);
// do stuff with the data
}
//
复制代码
如果使用 fs.readFile 读取给定路径时引发任何错误,我们将获得一个错误对象。这时我们可以:
像之前一样简单地记录错误对象。
抛出一个异常。
将这个错误传递给另一个回调。
要抛出异常,我们可以执行以下操作:
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);
}
复制代码
如果我们不想使程序崩溃,则将错误传递给另一个回调是首选方法:
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 是一个用于错误处理的简单函数:
function errorHandler(error) {
console.error(error.message);
// do something with the error:
// - write to a log.
// - send to an external logger.
}
复制代码
Node.js 中的异步错误处理:事件发射器
你在 Node.js 中所做的大部分工作都是基于 事件 的。大多数情况下,你会与 发射器对象 和一些观察者交互以侦听消息。
Node.js 中的任何事件驱动模块(例如 net)都扩展了一个名为 EventEmitter 的根类。
Node.js 中的 EventEmitter 有两种基本方法:on 和 emit。
考虑以下简单的 HTTP 服务器:
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。除了这些事件之外,事件发射器还在出现错误时公开一个 错误 事件。
如果你在端口 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: ...
复制代码
要捕获它,我们可以注册一个 错误 事件处理器:
server.on("error", function(error) {
console.error(error.message);
});
复制代码
这会 Print:
listen EACCES: permission denied 127.0.0.1:80
复制代码
此外,该程序不会崩溃。要了解有关该主题的更多信息,请参考“Node.js 中的错误处理”。
https://www.joyent.com/node-js/production/design/errors
总结
在本指南中,我们涵盖了从简单同步代码到高级异步原语的 JavaScript 错误处理完整概念。
在我们的 JavaScript 程序中,可以通过多种方式来显示异常。
同步代码中的异常是最容易捕获的。相反,异步代码 路径中的 异常 可能很难处理。
同时,浏览器中的新 JavaScript API 几乎都通向 Promise。这种普遍的模式使我们更容易用 then/catch/finally 或 try/catch 对 async/await 处理异常。
阅读本指南后,你应该能够 识别程序中可能出现的所有不同情况,并正确捕获 异常。
感谢你的阅读和关注!
英文原文
A mostly complete guide to error handling in JavaScript
评论