目录
编程中有什么错误?
JavaScript 中有什么错误?
JavaScript 中的错误类型
什么是异常?
当抛出异常时会发生什么?
同步错误处理
常规函数的错误处理
生成器函数的错误处理
异步错误处理
计时器错误处理
事件的错误处理
How about onerror?
怎么处理 onerror?
用 Promise 处理错误
Promise, error 和 throw
错误处理 “promisified” 计时器
Promise.all 中的错误处理
Promise.any 中的错误处理
Promise.race 中的错误处理
Promise.allSettled 中的错误处理
async/await 的错误处理
异步生成器的错误处理
Node.js中的错误处理
Node.js 中的同步错误处理
Node.js 中的异步错误处理:回调模式
Node.js 中的异步错误处理:事件发射器
总结
编程中有什么错误?
在我们的程序中,事情并非一帆风顺。
特别是在某些情况下,我们可能希望在停止程序或在发生不良状况时通知用户。例如:
程序试图打开一个不存在的文件。
网络连接断开。
用户进行了无效的输入。
在所有的这些情况下,我们作为程序员都会产生错误,或者让编程引擎为我们创建一些错误。
在创建错误之后,我们可以向用户通知消息,或者可以完全停止执行。
JavaScript 中有什么错误?
JavaScript 中的错误是一个对象,随后被抛出,用以终止程序。
要在 JavaScript 中创建新错误,我们调用相应的构造函数。例如,要创建一个新的通用错误,可以执行以下操作:
创建错误对象时,也可以省略关键字 :
创建后,错误对象将显示三个属性:
:带有错误信息的字符串。
:错误的类型。
:函数执行的栈跟踪。
例如,如果我们用适当的消息创建一个新的 对象,则 将携带实际的错误字符串,而 则为 :
Firefox 还实现了一堆非标准属性,例如 , 和 。
JavaScript 中的错误类型
JavaScript 中有很多类型的错误,即:
请记住,所有这些错误类型都是实际构造函数,旨在返回一个新的错误对象。
在代码中主要用 和 这两种最常见的类型来创建自己的错误对象。
但是在大多数情况下,很多错误直接来自 JavaScript 引擎,例如 或 。
下面的例子是当你尝试重新为 赋值时,将触发 :
当你关键字拼错时,就会触发 :
或者,当你在错误的地方使用保留关键字时,例如在 函数之外的使用 :
当在页面中选择不存在的 HTML 元素时,会发生 :
除了这些“传统的”错误对象外, 对象也即将能够在 JavaScript 中使用。
可以把多个错误很方便地包装在一起,在后面将会看到。
除了这些内置错误外,在浏览器中还可以找到:
已弃用,目前不再使用。
是与 Web API 相关的一系列错误。有关完整列表,请参见 MDN。
什么是异常?
很多人认为错误和异常是一回事。实际上错误对象仅在抛出时才成为异常。
要在 JavaScript 中引发异常,我们使用 关键字,后面跟错误对象:
更常见的是缩写形式,在大多数代码库中,你都可以找到:
或者:
一般不会把异常抛出到函数或条件块之外,当然也有例外情况,例如:
在代码中我们检查函数的参数是否为字符串,如果不是则抛出异常。
从技术上讲,你可以在 JavaScript 中抛出任何东西,而不仅仅是错误对象:
但是,最好不要这样做,应该总是抛出正确的错误对象,而不是原始类型。
这样就可以通过代码库保持错误处理的一致性。其他团队成员总是能够在错误对象上访问 或 。
当抛出异常时会发生什么?
异常就像电梯在上升:一旦抛出一个异常,它就会在程序栈中冒泡,除非被卡在某个地方。
看下面的代码:
如果你在浏览器或 Node.js 中运行这段代码,程序将停止并报告错误:
另外还可以看到发生错误的代码行数。
这段报告是栈跟踪(stack trace),对于跟踪代码中的问题很有帮助。
栈跟踪从底部到顶部。所以是这样的:
我们可以说:
程序的第 9 行中名为 的内容
在第 3 行引发了一个问题
除了在浏览器的控制台中看到栈跟踪之外,还可以在错误对象的 属性上对其进行访问。
如果异常是未捕获的,也就是说程序员没有采取任何措施来捕获它,则程序将会崩溃。
你在什么时候及在什么地方捕获代码中的异常取决于特定的用例。
例如,你可能想要在栈中传播异常,使程序完全崩溃。当发生致命的错误,需要更安全地停止程序而不是处理无效数据时,你可能需要这样做。
介绍了基础知识之后,现在让我们将注意力转向同步和异步 JavaScript 代码中的错误和异常处理。
同步错误处理
同步代码通常很简单,它的错误处理也是如此。
常规函数的错误处理
同步代码按照代码顺序按部就班的执行。让我们再来看前面的例子:
在这里,引擎调用并执行 。所有操作都同步进行。要捕获由这种同步函数产生的异常,可以用 :
通常 处理主处理流程或者可能引发异常的函数调用。
而 则捕获实际的异常。它接收错误对象,可以在这里对其进行检查(并远程发送到生产环境中的日志服务器)。
另外无论函数的执行结果如何,不管是成功还是失败, 中的所有代码都会被执行。
请记住: 是一个同步结构:它可以捕获来自异步代码的异常。
生成器函数的错误处理
JavaScript 中的生成器函数是一种特殊的函数。
除了在其内部作用域和使用者之间提供双向通信通道之外,它还可以随意暂停和恢复。
要创建一个生成器函数,需要在关键字 之后加一个星号 :
进入函数后,可以使用 返回值:
生成器函数的返回值是迭代器对象。有两种方法从生成器中提取值:
在迭代器对象上调用 。
iterationwith .
带有 的迭代。
以上面的代码为例,要从生成器获取值,可以这样做:
当调用生成器函数时, 成了我们的迭代器对象。
现在可以调用 来执行:
生成器也可以通过其他方式工作:它们可以接受调用者返回的值和异常。
除了 外,从生成器返回的迭代器对象还有 方法。用这个方法,可以通过把异常注入到生成器来暂停程序:
可以用 (和 ,如果需要的话)将代码包装在生成器中来捕获这样的错误:
生成器函数还可以向外部抛出异常。捕获这些异常的机制与捕获同步异常的机制相同:。
下面是通过 从外部使用的生成器函数的例子:
代码中迭代 块内的主处理流程。如果发生任何异常,就用 停止。
异步错误处理
JavaScript 在本质上是同步的,是一种单线程语言。
诸如浏览器引擎之类的环境用许多 Web API 增强了 JavaScript,用来与外部系统进行交互并处理与 I/O 绑定的操作。
浏览器中的异步示例包括timeouts、events、Promise。
异步代码中的错误处理与同步代码不同。
看一些例子:
计时器错误处理
在你开始学习 JavaScript 时,当学 之后,你可能会想把它们放在任何代码块中。
思考下面的代码段:
这个函数将在大约 1 秒钟后被触发。那么处理这个异常的正确方式是什么?
下面的例子是无效的:
正如前面所说的, 是同步的。另一方面,我们有 ,这是一个用于定时器的浏览器 API。
到传递给 的回调运行时, 已经“消失了”。程序将会崩溃,因为我们无法捕获异常。
它们在两条不同的轨道上行驶:
如果我们不想使程序崩溃,为了正确处理错误,我们必须把 移动到 的回调中。
但是这在大多数情况下并没有什么意义。Promises 的异步错误处理提供了更好的方式。
事件的错误处理
文档对象模型中的HTML节点连接到 , 是浏览器中所有 event emitter 的共同祖先。
这意味着我们可以侦听页面中任何 HTML 元素上的事件。Node.js 将在未来版本中支持 。
DOM 事件的错误处理机制遵循与异步 Web API 的相同方案。
看下面的例子:
在这里,单击按钮后会立即引发异常,应该怎样捕获它?下面的方法不起作用,而且不会阻止程序崩溃:
与前面的带有 的例子一样,传递给 的任何回调均异步执行:
如果不想使程序崩溃,为了正确处理错误,必须把 放在 的回调内。但这样做没有任何价值。与 一样,异步代码路径引发的异常从外部是无法捕获的,这将会使程序崩溃。
How about onerror?
怎么处理 onerror?
HTML 元素具有许多事件处理函数,例如 、 和 等。
还有 ,但是它与 没有什么关系。
每当像 标签或 之类的 HTML 元素遇到不存在的资源时, 事件处理函数都会触发。
看下面的例子:
当访问缺少或不存在资源的 HTML 文档时,浏览器的控制台会输出以下错误:
在 JavaScript 中,我们有机会使用适当的事件处理程序来“捕获”这个错误:
或者用更好的方法:
此模式可用于加载替代资源来替换丢失的图像或脚本。
但是要记住: 与 或 无关。
用 Promise 处理错误
为了说明 Promise 的错误处理,我们将 “Promise” 前面的一个例子。调整以下功能:
为了代替返回简单的字符串或异常,可以分别用 和 处理错误和成功:
从技术上讲,这段代码中没有异步的东西,但是它能很好地说明这一点。
现在函数已 “promise 化”,我们可以通过 使用结果,并附加 来处理被拒绝的Promise:
这段代码将会输出:
在 Promise 领域, 是用于处理错误的结构。
除了 和 之外,还有 ,类似于 中的 。
相对于同步而言,Promise 的 运行与 Promise 结果无关:
切记,传递给 的任何回调都是由微任务队列异步处理的。微任务优先于宏任务,例如事件和计时器。
Promise, error 和 throw
作为拒绝 Promise 的最佳方法,提供错误对象很方便:
这样,你可以通过代码库保持错误处理的一致性。其他团队成员总是可以期望访问 ,更重要的是你可以检查栈跟踪。
除了 之外,可以通过抛出异常来退出 Promise 链。
看下面的例子:
我们用一个字符串解决一个 Promise,然后立即用 打破这个链。
为了阻止异常的传播,照常使用 :
这种模式在 中很常见,我们在其中检查响应对象并查找错误:
在这里可以用 拦截异常。如果失败了,或者决定不去捕获它,则异常可以在栈中冒泡。
从本质上讲,这还不错,但是在不同的环境下对未捕获的 rejection 的反应不同。
例如,将来的 Node.js 将使任何未处理 Promise rejection 的程序崩溃:
更好地捕获他们!
错误处理 “promisified” 计时器
使用计时器或事件无法捕获从回调引发的异常。我们在上一节中看到了例子:
Promise 提供的解决方案在于代码的“promisification”。基本上,我们用 Promise 包装计时器:
通过 ,我们启动了Promise rejection,它带有一个错误对象。
这时可以用 处理异常:
注意:通常使用 作为 Promise 的返回值,并用 作为 rejection 的返回对象。
Node.js 有一个名为promisify的工具函数,可以简化旧式回调 API 的“混杂”。
Promise.all 中的错误处理
静态方法 接受一个 Promise 数组,并返回所有解析 Promise 的结果数组:
如果这些 Promise 中的任何一个被拒绝, 都会拒绝,并返回第一个被拒绝的 Promise 中的错误。
为了在 中处理这些情况,需要使用 ,就像在上一节中所做的那样:
要再次运行函数而不考虑 的结果,我们可以使用 。
Promise.any 中的错误处理
我们可以将 (Firefox> 79,Chrome> 85)视为与 相反。
即使数组中的一个 Promise 拒绝, 也会返回失败,而 总是提供第一个已解决的Promise(如果存在于数组中),无论发生了什么拒绝。
如果传递给 的 Promise 不是都被拒绝,则产生的错误是 。考虑以下示例:
这里用 处理错误。这里代码的输出是:
对象有与基本 相同的属性,以及 属性:
这个属性是拒绝产生的每个错误的数组:
Promise.race 中的错误处理
静态方法 接受一个 Promise 数组:
结果是第一个赢得“race”的 Promise。
那 rejection 呢?如果拒绝的 Promise 不是第一个出现在输入数组中的对象,则 解析:
如果rejection 出现在数组的第一个元素中,则 被拒绝,我们必须捕获它:
Promise.allSettled 中的错误处理
是对该语言的 ECMAScript 2020 补充。
这个静态方法没有什么要处理的,因为**即使一个或多个输入 Promise 被拒绝,结果也始终是一个已解决的Promise **。
看下面的例子:
我们将由两个 Promise 组成的数组传递给 :一个已解决,另一个被拒绝。
在这种情况下, 将永远不会被执行。 会运行。
日志输出的 的代码的结果是:
async/await 的错误处理
JavaScript 中的 表示异步函数,但从维护者的角度来看,它们受益于同步函数的所有“可读性”。
为了简单起见,我们将使用先前的同步函数 ,并将 放在 关键字之前,将其转换为异步函数:
只需在函数前面加上 ,就可以使函数返回一个Promise。这意味着我们可以在函数调用之后链接 ,和 :
当我们从异步函数中抛出异常时,异常会成为导致底层 Promise 被拒绝的原因。
任何错误都可以通过外部的 来拦截。
最重要的是,除了这种样式外,还可以使用 ,就像使用同步函数一样。
在下面的例子中,我们从另一个函数 调用 ,该函数用 方便地包装函数调用:
输出为:
异步生成器的错误处理
JavaScript 中的异步生成器(Async generators)不是生产简单值,而是能够生成 Promise 的生成器函数 。
它们将生成器函数与 结合在一起。其结果是生成器函数将 Promise 暴露给使用者的迭代器对象。
我们用前缀为 和星号 声明一个异步生成器函数。
基于 Promise 用于错误处理的相同规则,异步生成器中的 导致 Promise 拒绝,用 进行拦截。
有两种方法可以把 Promise 拉出异步生成器:
。
异步迭代。
从上面的例子中,在前两个 之后会有一个例外。这意味着我们可以做到:
这段代码的输出是:
另一种方法是使用异步迭代和 。要使用异步迭代,需要用 函数包装使用者。
这是完整的例子:
和 一样,可以用 处理任何潜在的异常:
这段代码的输出是:
从异步生成器函数返回的迭代器对象也有一个 方法,非常类似于它的同步对象。
在这里的迭代器对象上调用 不会引发异常,但是会被 Promise 拒绝:
可以通过执行以下操作从外部处理这种情况:
但是,别忘了迭代器对象 在生成器内部发送异常。这意味着我们还可以用以下模式:
Node.js中的错误处理
Node.js 中的同步错误处理
Node.js 中的同步错误处理与到目前为止所看到的并没有太大差异。
对于同步代码, 可以正常工作。
但是如果进入异步世界,事情就会变得有趣。
Node.js 中的异步错误处理:回调模式
对于异步代码,Node.js 强烈依赖于两个习惯用法:
回调模式。
事件发射器(event emitter)。
在回调模式中,异步 Node.js API 接受通过事件循环处理的函数,并在调用栈为空时立即执行。
看下面的代码:
如果从这个清单中提取回调,则可以看到应该如何处理错误:
如果通过使用 读取给定路径而引起任何错误,将得到一个错误对象。
在这一点上,我们可以:
简单地把错误对象输出到日志。
引发异常。
将错误传递给另一个回调。
要抛出异常,可以执行以下操作:
但是,与 DOM 中的事件和计时器一样,这个异常将会使程序崩溃。下面的代码尝试通过 的处理将不起作用:
如果不想使程序崩溃,则首选项是将错误传递给另一个回调:
顾名思义,是一个简单的错误处理函数:
Node.js 中的异步错误处理:事件发射器
我们在 Node.js 中所做的大部分工作都是基于事件的。在大多数情况下,需要与发射器对象和一些观察者侦听消息进行交互。
Node.js 中的任何事件驱动模块(例如net)都会扩展名为 EventEmitter 的根类 。
Node.js中的 有两种基本方法: 和 。
看下面这个简单的 HTTP 服务器:
在这里,我们监听两个事件:listening和connection。
除了这些事件之外,事件发射器还暴露了error事件,以防发生错误。
在 80 端口上运行代码,会得到一个异常:
输出:
要捕获它,可以为error注册一个事件处理函数:
这将会输出:
并且程序不会崩溃。
总结
在本文中,我们介绍了从简单的同步代码到高级异步原语,以及整个 JavaScript 的错误处理。
在 JavaScript 程序中,可以通过多种方式来显示异常。
同步代码中的异常是最容易捕获的。而来自异步代码路径的异常处理可能会有些棘手。
同时,浏览器中的新 JavaScript API 几乎都朝着 的方向发展。这种普遍的模式使得用 或用 来处理 异常更加容易。
看完本文后,你应该能够识别程序中可能会出现的所有不同情况,并正确捕获异常。
领取专属 10元无门槛券
私享最新 技术干货