Jav登贝莱被梅西夸赞我倍感荣幸战多特地义特殊aScript毛病处理完全指南

  • A+
所属分类:Yaboapp下载
摘要

作者 | Valentino Gagliardi译者 | 王强策划 | 小智本文最初发布于 valentinog.com 网站,经原作者授权由 InfoQ 中文

作者 | Valentino Gagliardi

译者 | 王强

策划 | 小智

本文最初发布于 valentinog.com 网站,经原作者授权由 InfoQ 中文站翻译并分享。

1

甚么是编程中的毛病?

在我们的程序中,事物并不是总是1帆风顺的。

特别是在某些情况下,我们可能希望停止程序或在产生意外毛病时通知用户。

例如:

程序试图打开1个不存在的文件

网络连接断开

用户输入了无效的内容

在所有这些情况下,我们程序员都会创建毛病,或让编程引擎为我们创建1些毛病。

在创建毛病以后,我们可以向用户发送1条消息,或完全停止履行。

2

JavaScript 中有甚么毛病?

JavaScript 中的1个毛病是1个对象,毛病会被抛出以暂停程序。

要在 JavaScript 中创建1个新毛病,我们需要调用适当的构造函数。例如,要创建1个新的泛型毛病,我们可以履行以下操作:

创建1个毛病对象时,也能够省略 new 关键字:

创建后,毛病对象将显示3个属性:

message:包括毛病消息的字符串

name:毛病的类型

stack:函数履行的堆栈跟踪

例如,如果我们创建1个新的 TypeError 对象,带有适当的消息,该 message 将携带实际的毛病字符串,而 name 将为“TypeError”:

Firefox 还实现了1些非标准属性,如 columnNumber、filename 和 lineNumber。

3

JavaScript 中的毛病类型

JavaScript 中有很多毛病类型,包括:

Error

EvalError

InternalError

RangeError

ReferenceError

SyntaxError

TypeError

URIError

请记住,所有这些毛病类型都是实际的构造函数,旨在返回1个新的毛病对象。

在代码中,你将主要使用 Error 和 TypeError 这两种最多见的类型来创建自己的毛病对象。

1般来讲,大多数毛病将直接来自 JavaScript 引擎,例如 InternalError 或 SyntaxError。

当你尝试重赋值 const 时,会产生 TypeError:

当你的语言关键字拼写毛病时,会产生 SyntaxError:

或,当你在毛病的地方使用保存的关键字时,例如在1个 async 函数外部 await:

当我们在页面当选择不存在的 HTML 元素时,也会产生 TypeError:

除这些传统的毛病对象外,JavaScript 中很快还会有 AggregateError 对象。AggregateError 可以很容易地将多个毛病包装在1起,后文会具体介绍。

除这些内置毛病外,在阅读器中我们还可以找到:

DOMException

DOMError,已弃用,如今不再使用

DOMException 是与 WebAPI 相干的1系列毛病。当我们在阅读器中做蠢事时它们就会被抛出,例如:

结果:

有关完全列表,请参见 MDN 上的这1页面:

https://developer.mozilla.org/en-US/docs/Web/API/DOMException

4

甚么是异常

多数开发人员认为毛病和异常是同1回事。实际上,1个毛病对象只有在被抛出时才成为异常。

要在 JavaScript 中抛出1个异常,我们使用 throw,然后是毛病对象:

缩写情势更常见,在大多数代码库中你都可以找到:

或:

不太可能将异常抛出到函数或条件块以外。相反,斟酌以下示例:

在这里,我们检查这个函数参数是不是为1个字符串。如果不是,我们抛出1个异常。从技术上讲,你可以在 JavaScript 中抛出任何内容,而不单单是毛病对象:

但最好避免这些事情,始终抛出正确的毛病对象,而不是基元。这样,你就能够在代码库中保持毛病处理的1致性。其他团队成员就可以1直在毛病对象上访问 error.message 或 error.stack。

5

当我们抛出异常时会产生甚么?

异常就像在上升的电梯:1旦抛出1个,它就会在程序栈中冒泡,除非它在某个地方被捕获。

斟酌以下代码:

如果你在阅读器或 Node.js 中运行此代码,程序将停止并报告毛病:

另外,你可以看到产生毛病的具体代码行。这个报告是1个堆栈跟踪,对跟踪代码中的问题很有帮助。

堆栈跟踪的顺序是从底到顶的。所以在这里:

我们可以说:

第 9 行中的代码调用了 toUppercase

toUppercase 在第 3 行爆炸了

除在阅读器的控制台中看到这个堆栈跟踪外,你还可以在毛病对象的 stack 属性上访问它。

如果这个异常未捕获,即程序员没有采取任何措施来捕获它,则程序将崩溃。

在什么时候何地捕获代码中的异常取决于具体的用例。

例如,你可能想在堆栈中传播1个异常,以使程序完全崩溃。出现致命的毛病时可能就会是这类情况,由于停止程序比处理无效数据更安全。

介绍了基础知识以后,现在我们来研究同步和异步 JavaScript 代码中的毛病和异常处理。

6

同步毛病处理

同步代码在大多数情况下很简单,它的毛病处理也是如此。

常规函数的毛病处理

同步代码的履行顺序和代码的编写顺序1致。再来看前面的示例:

在这里,引擎调用并履行 toUppercase。所有这些都是同步产生的。要捕获由此类同步函数引发的异常,我们可使用 try/catch/finally:

通常,try 处理最简单的场景,或可能抛出毛病的函数调用。catch 则会捕获实际的异常。它接收毛病对象,我们可以检查该毛病对象(并将其远程发送到生产环境中的某些记录器)。

另外一方面,不管函数的结果如何,finally 语句都会运行:不管是失败还是成功,final 内部的任何代码都将运行。

记住:try/catch/finally 是1个同步结构:它现在具有捕获来自异步代码异常的方法。

生成器函数的毛病处理

JavaScript 中的生成器(generator)函数是1种特殊的函数。

除在其内部作用域軍情和消费者之间提供双向通讯通道外,它可以随便暂停和恢复。

要创建1个生成器函数,我们在 function 关键字后加1个星号 *:

1旦进入函数,我们就能够使用 yield 来返回值:

生成器函数的返回值是迭代器(iterator)对象。为了从生成器中提取值,我们可使用两种方法:

在迭代器对象上调用 next()

该言论也引发很多FPX粉丝反感,随后经过网友多方调查以后,发现这个网友是炫神粉丝,接下来就引发了炫神与FPX粉丝在超话中的互怼。

for...of 的迭代

以我们的示例为例,要从生成器获得值,我们可以这样做:

当我们调用生成器函数时,go 成为我们的迭代器对象。从现在开始,我们可以调用 go.next() 来推动履行:

生成器也有另外一种工作机制:它们可以接受调用者返回的值和异常。除 next() 以外,从生成器返回的迭代器对象还具有 throw() 方法。

使用这类方法,我们可以将异常注入生成器来暂停程序:

要捕获此类毛病,你可使用 try/catch 将代码包装在生成器中(如果需要的话也能够用 finally):

生成器函数还可以向外部抛出异常。捕获这些异常的机制与捕获同步异常的机制相同:try/catch/finally。

这是1个从外部使用 for...of 消费的生成器函数的示例:

在这里,我们迭代 try 块中的 happy path。如果产生任何异常,我们将使用 catch 停止它。

7

异步毛病处理

JavaScript 本质上是同步的,是1种单线程语言。

阅读器引擎之类的主机环境使用许多 WebAPI 增强了 JavaScript,以同外部系统交互并处理 I/O 相干联的操作。

阅读器中的异步性示例包括超时、事件和 Promise。

异步世界中的毛病处理与同步世界是不1样的。

我们来看1些例子。

计时器毛病处理

开始探索 JavaScript 时,在学习了 try/catch/finally 以后,你可能会想将它放在任何代码块中。

斟酌以下代码段:

此函数将在大约 1 秒钟后抛出毛病。处理此异常的正确方法是甚么?以下示例不起作用:

正如我们所说,try/catch 是同步的。另外一方面,我们有 setTimeout,这是1个用于计时器(timer)的阅读器 API。到传递给 setTimeout 的回调运行时,我们的 try/catch早就没了。该程序将崩溃,由于我们没法捕获异常。

101屡次被网友诟病解说的业务能力,此次亦虚心请教,回答网友大部份的私信。

它们走的是两条不同的路径:

如果我们不想让程序崩溃,为了正确处理毛病,我们必须在 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 的模式。

斟酌以下示例:

在这里,单击按钮后立即抛出1个异常。我们如何捕获它呢?这个模式不起作用,也不会禁止程序崩溃:

与前面带有 setTimeout 的示例1样,传递给 addEventListener 的任何回调均异步履行:

如果我们不希望程序崩溃,则要正确处理毛病,我们必须在 addEventListener 的回调中移动 try/catch。但一样,这样做几近没有任何价值。

与 setTimeout 1样,异步代码路径抛出的异常没法从外部捕获,这将使程序崩溃。

在下1部份中,我们将了解如何使用 Promises 和 async/await 简化异步代码的毛病处理。

onerror 是甚么情况?

HTML 元素有许多事件处理器,例如 onclick、onmouseenter、onchange 等。

还有 onerror,但它与 throw 之类是无关的。

每当标签或之类的 HTML 元素遇到不存在的资源时,onerror 事件处理器都会触发。

斟酌以下示例:

当访问缺少资源或不存在资源的 HTML 文档时,阅读器的控制台会记录以下毛病:

在 JavaScript 中,我们可使用适当的事件处理器来“捕获”此毛病:

更好的是:

此模式可以方便地加载替换资源来代替丢失的图象或脚本。但是请记住:onerror 与 throw 或 try/catch 无关。

使用 Promise 处理毛病

为了说明用 Promise 处理毛病的机制,我们将“Promise”我们的1个原始示例。调剂以下函数:

这里我们不会返回简单的字符串或异常,而是分别使用 Promise.reject 和 Promise.resolve 处理毛病和成功情况:

(从技术上讲这段代码中没有异步的内容,但它很好地展现了具体的机制)。

现在函数已“Promise 化”,我们可以附加 then 来消费结果,并 catch 以处理被谢绝的 Promise:

代码会记录:

在 Promise 的世界中,catch 是用于处理毛病的结构。除 catch 和 then,我们也有 finally,类似于 try/catch 中的 finally。

作为其除此以外,西卡也在微博进行了道歉。同步的“相对”,Promise 的 finally不管Promise 的结果如何都会运行:

谨记,传递给 then/catch/finally 的任何回调都是由微任务队列异步处理的。它们是微任务,优先于事件和计时器等宏任务。

Promise,毛病和抛出

作为谢绝 Promise 的最好实践,提供毛病对象很方便:

这样,你可以在代码库中保持毛病处理的1致性。其他团队成员总是能访问 error.message,更重要的是你可以检查堆栈跟踪。除 Promise.reject,我们还可以通过抛出异常来退出 Promise 链。

斟酌以下示例:

我们用1个字符串解析1个 Promise,然后 Promise 链会立刻被 throw 断开。要停止异常传播,我们照旧使用 catch:

这类模式在 fetch 中很常见,我们在 fetch 中检查响应对象以查找毛病:

在这里,异常可以被 catch 拦截。如果我们失败了,或决定不在这里捕获它,那末异常就能够在堆栈中冒泡了。这本身其实不坏,但是不同的环境对未捕获的谢绝的反应是不同的。

例如,将来的 Node.js 将使任何未处理 Promise 谢绝的程序崩溃:

所以最好捕获它们!

“Promise 化”计时器的毛病处理

使用计时器或事件没法捕获从回调抛出的异常。我们在上1节中看到了1个示例:

Promise 提供的1个解决方案是代码的“Promise 化”。具体来讲,我们用 Promise 包装计时器:

通过 reject,我们启动了1个 Promise 谢绝,带有1个毛病对象。这时候,我们可使用 catch 处理异常:

注意:通常使用 value 作为 Promise 的返回值,并使用 reason 作为谢绝的返回对象。

Node.js 有1个名为 promisify 的实用程序,可简化旧式回调 API 的“Promise 化”。

https://nodejs.org/api/util.html#util\_util\_promisify\_original

Promise.all 中的毛病处理

静态方法 Promise.all 接收1个 Promise 数组,并从所有解析中的 Promise 返回1个结果数组:

如果这些 Promise 中的任何1个被谢绝,Promise.all 都会谢绝,并返回第1个被谢绝的 Promise 中的毛病。为了在 Promise.all 中处理这些情况,我们像上1节中1样使用 catch:

一样,不管 Promise.all 的结果如何都要运行函数的话,我们可使用 finally:

Promise.any 中的毛病处理

我们可以将 Promise.any(Firefox>79,Chrome>85)视为 Promise.all 的反面。

即便数组中只有1个 Promise 谢绝,Promise.all 也会返回失败;而 Promise.any 始终为我们提供第1个已解析的 Promise(如果存在于数组中),不管产生了甚么谢绝。

如果所有传递给 Promise.any 的 Promise 都谢绝,则产生的毛病是 AggregateError。斟酌以下示例:

在这里,我们使用 catch 处理毛病。此代码的输出是:

AggregateError 对象具有与基础的 Error 相同的属性,外加1个 errors 属性:

此属性是由谢绝产生的各个毛病组成的数组:

Promise.race 中的毛病处理

静态方法 Promise.race 接收1个 Promise 数组:

结果是第1个赢得“比赛”的 Promise。那谢绝呢?如果谢绝的 Promise 不是第1个出现在输入数组中的对象,则 Promise.race 解析:

如果谢绝出现在数组的第1个元素中,则 Promise.race 谢绝,且我们必须捕获这个谢绝:

Promise.allSettled 中的毛病处理

Promise.allSettled 是 ECMAScript 2020 加入的。

使用这类静态方法没有甚么要处理的,由于即便1个或多个输入 Promise 谢绝,结果始终是1个已解析的 Promise。

斟酌以下示例:

我们传递给 Promise.allSettled 1个由两个 Promise 组成的数组:1个已解析,另外一个被谢绝。在这类情况下,catch 将永久不会启用。因而会运行 finally。

代码的结果记录在 then 中,以下:

async/await 的毛病处理

JavaScript 中的 async/await 表示异步函数,但是从读者的角度来看,它们也具有同步函数的所有可读性。

为简单起见,我们将先前的同步函数设为 Uppercase,并在 function 关键字之前放置 async,以将其转换为异步函数:

只需在函数前面加上 async 前缀,我们就能够使函数返回1个 Promise。这意味着我们可以在函数调用以后来1串 then、catch 和 finally:

当我们从1个 async 函数中抛出异常时,异常将成为底层 Promise 被谢绝的缘由。

可使用 catch 从外部拦截任何毛病。

最重要的是,除这类样式外,我们还可使用try/catch/finally,就像我们使用同步函数时所做的1样。

在下面的示例中,我们从另外一个函数 consumer 调用 toUppercase,前者方便地用 try/catch/finally 将函数调用包装起来:

输出是:

同1主题的资料:如何从 JavaScript 中的 async 函数抛出毛病?

https://www.valentinog.com/blog/throw-async/

异步生成器的毛病处理

JavaScript 中的异步生成器是能够生成 Promise 而非简单值的生成器函数。

它们将生成器函数与 async 结合在1起。结果是1个生成器函数,其迭代器对象将1个 Promise 暴露给消费者。

要创建1个异步生成器,我们用星号 * 声明1个生成器函数,加1个 async 前缀:

此处的毛病处理规则也是和 Promise 1样的。在异步生成器中 throw 将致使1个 Promise 谢绝,我们使用 catch 拦截它。要从异步生成器拉出 Promise,我们可使用两种方法:

then 处理器

async 迭代

从上面的示例中,我们可以肯定地知道在前两个 yield 以后会有1个异常。也就是说我们可以:

代码输出是:

另外一种方法是使用 for await...of 的async 迭代。要使用 async 迭代,我们需要使用1个 async 函数包装这个消费者。

下面是完全的示例:

与 async/await 1样,我们使用 try/catch 处理任何潜伏的异常:

代码输出是:

从异步生成器函数返回的迭代器对象也有1个 throw() 方法,非常像它的同步情势。在此处的迭代器对象上调用 throw() 不会抛出异常,而是1个 Promise 谢绝:

要从外部处理这类情况,我们可以履行以下操作:

但是,请不要忘记迭代器对象 throw()在生成器内部发送异常。也就是说我们还可以利用以下模式:

8

Node.js 中的毛病处理

Node.js 中的同步毛病处理

Node.js 中的同步毛病处理与上文介绍的内容并没有太大差异。

对同步代码,try/catch/finally 没甚么问题。

但如果我们进入异步世界,事情就会变得很有趣了。

Node.js 中的异步毛病处理:回调模式

对异步代码,Node.js 强烈依赖两个习惯用法:

回调模式

事件发射器

在回调模式中,异步 Node.jsAPI 接收1个函数,该函数通过事件循环处理,并在调用堆栈为空时立即履行。

斟酌以下代码:

如果从此清单中提取回调,就能够看到毛病的处理方式:

如果使用 fs.readFile 读取给定路径时引发任何毛病,我们将取得1个毛病对象。这时候我们可以:

像之前1样简单地记录毛病对象

抛出1个异常

将这个毛病传递给另外一个回调

要抛出异常,我们可以履行以下操作:

但是,与 DOM 中的事件和计时器1样,此异常将使程序崩溃。尝试使用 try/catch 停止它的方法将不起作用:

如果我们不想使程序崩溃,则将毛病传递给另外一个回调是首选方法:

顾名思义,errorHandler 是1个用于毛病处理的简单函数:

你在 Node.js 中所做的大部份工作都是基于事件的。大多数情况下,你会与发射器对象和1些视察者交互以侦听消息。

Node.js 中的任何事件驱动模块(例如 net)都扩大了1个名为 EventEmitter 的根类。

Node.js 中的 EventEmitter 有两种基本方法:on 和 emit。

斟酌以下简单的 HTTP 服务器:

在这里我们监听两个事件:listening 和 connection。除这些事件以外,事件发射器还在出现毛病时公然1个毛病事件。

如果你在端口 80 上运行此代码,则会得到1个异常:

输出:

要捕获它,我们可以注册1个毛病事件处理器:

这会 Print:

另外,该程序不会崩溃。要了解有关该主题的更多信息,请参考“Node.js 中的毛病处理”。

https://www.joyent.com/node-js/production/design/errors

9

总结

在本指南中,我们涵盖了从简单同步代码到高级异步原语的JavaScript 毛病处理完全概念。

在我们的 JavaScript 程序中,可以通过量种方式来显示异常。

同步代码中的异常是最容易捕获的。相反,异步代码路径中的异常可能很难处理。

同时,阅读器中的新 JavaScript API 几近都通向 Promise。这类普遍的模式使我们更容易用 then/catch/finally 或 try/catch 对 async/await 处理异常。

浏览本指南后,你应当能够辨认程序中可能出现的所有不同情况,并正确捕获异常。

感谢你的浏览和关注!

英文原文:

A mostly complete guide to error handling in JavaScript

https://www.valentinog.com/blog/error/

本周公众号宝藏文章

点个在看少个 bug

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: