JavaScript错误处理完全指南

JavaScript错误处理完全指南
2020年09月20日 10:04 InfoQ

作者| Valentino Gagliardi

译者 | 王强

策划 | 小智

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

1

什么是编程中的错误?

在我们的程序中,事物并非总是一帆风顺的

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

例如:

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

网络连接断开

用户输入了无效的内容

在所有这些情况下,我们程序员都会创建 错误,或者让编程引擎为我们创建一些错误。

在创建错误之后,我们可以向用户发送一条消息,或者完全停止执行。

2

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。

3

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 上的这一页面:

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

4

什么是异常?

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

要在 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。

5

当我们抛出异常时会发生什么?

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

考虑以下代码:

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

http://localhost:5000/index.js:9

此外,你可以看到发生错误的具体代码行。这个报告是一个 堆栈跟踪,对于跟踪代码中的问题很有帮助。

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

toUppercase http://localhost:5000/index.js:3

http://localhost:5000/index.js:9

我们可以说:

第 9 行中的代码调用了 toUppercase

toUppercase 在第 3 行爆炸了

除了在浏览器的控制台中看到这个堆栈跟踪外,你还可以在错误对象的 stack 属性上访问它。

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

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

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

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

6

同步错误处理

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

常规函数的错误处理  

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

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 停止它。

7

异步错误处理

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 简化异步代码的错误处理。

标签或

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部