错误处理与调试
JavaScript 一直以来被认为是最难调试的编程语言之一,因为它是动态的,且多年来没有适当的开发工具。错误经常会以令人迷惑的浏览器消息形式抛出,比如"object expected"。这样的消息没有上下文,因此很难理解。ECMAScript 第 3 版致力于改进这个方面,引入了 try/catch
和 throw
语句, 20 以及一些错误类型,以帮助开发者在出错时正确地处理它们。几年后,JavaScript 调试器和排错工具开始在浏览器中出现。到了 2008 年,大多数浏览器支持一些 JavaScript 调试能力。
有了适当的语言和开发工具,Web 开发者如今已可以实现适当的错误处理并找到问题的原因。
浏览器错误报告
所有主 流桌面浏览器,包括 IE/Edge、Firefox、Safari、Chrome 和 Opera,都提供了向用户报告错误的机制。默认情况下,所有浏览器都会隐藏错误信息。一个原因是除了开发者之外这些信息对别人没什么用,另一个原因是网页在正常操作中报错的固有特性。
桌面控制台
所有现代桌面浏览器都会通过控制台暴露错误。这些错误可以显示在开发者工具内嵌的控制台中。在前面提到的所有浏览器中,访问开发者工具的路径是相似的。可能最简单的查看错误的方式就是在页面上单击鼠标右键,然后在上下文菜单中选择 Inspect(检查)或 Inspect Element(检查元素),然后再单击 Console(控制台)选项卡。
移动控制台
移动浏览器不会直接在设备上提供控制台界面。不过,还是有一些途径可以在移动设备中检查错误。
Chrome 移动版和 Safari 的 iOS 版内置了实用工具,支持将设备连接到宿主操作系统中相同的浏览器。然后,就可以在对应的桌面浏览器中查看错误了。这涉及设备之间的硬件连接,且要遵循不同的操作步骤,比如 Chrome 的操作步骤参见 Google Developers 网站的文章《Android 设备的远程调试入门》, Safari 的操作步骤参见 Apple Developer 网站的文章“Safari Web Inspector Guide”。
此外也可以使用第三方工具直接在移动设备上调试。Firefox 常用的调试工具是 Firebug Lite,这需要通过 JavaScript 的书签小工具向当前页面中加入 Firebug 脚本才可以。脚 本运行后,就可以直接在移动浏览器上打开调试界面。Firebug Lite 也有面向其他浏览器(如 Chrome)的版本。
错误处理
错误处理在编程中的重要性毋庸置疑。所有主流 Web 应用程序都需要定义完善的错误处理协议, 大多数优秀的应用程序有自己的错误处理策略,尽管主要逻辑是放在服务器端的。事实上,服务器端团队通常会花很多精力根据错误类型、频率和其他重要指标来定义规范的错误日志机制。最终实现通过简单的数据库查询或报告生成脚本就可以了解应用程序的运行状态。
try/catch 语句
ECMA-262 第 3 版新增了 try/catch 语句,作为在 JavaScript 中处理异常的一种方式。基本的语法如下所示,跟 Java 中的 try/catch 语句一样:
try {
// 可能出错的代码
} catch (error) {
// 出错时要做什么
}
如果 try 块中有代码发生错误,代码会立即退出执行,并跳到 catch 块中。catch 块此时接收到一个对象,该对象包含发生错误的相关信息。与其他语言不同,即使在 catch 块中不使用错误对象, 也必须为它定义名称。错误对象中暴露的实际信息因浏览器而异,但至少包含保存错误消息的 message 属性。ECMA-262 也指定了定义错误类型的 name 属性,目前所有浏览器中都有这个属性。
try {
window.someNonexistentFunction();
} catch (error) {
console.log(error.message);
}
finally 子句
try/catch
语句中可选的 finally 子句始终运行。如果 try 块中的代码运行完,则接着执行 finally 块中的代码。如果出错并执行 catch 块中的代码,则 finally 块中的代码仍执行。try 或 catch 块无法阻止 finally 块执行,包括 return 语句。比如:
function testFinally() {
try {
return 0;
} catch (e) {
return 1;
} finally {
return 2;
}
}
console.log(testFinally()); // 2
finally
块的存在导致 try 块中的 return 语句被忽略。 因此,无论什么情况下调用该函数都会返回 2。如果去掉 finally 子句,该函数会返回 0。如果写出 finally 子句,catch 块就成了可选的(它们两者中只有一个是必需的)。
错误类型
代码执行过程中会发生各种类型的错误。每种类型都会对应一个错误发生时抛出的错误对象。 ECMA-262 定义了以下 8 种错误类型:
错误类型 | 描述 | 使用场景 |
---|---|---|
Error | 所有错误类型的基类。通常用于自定义错误或表示通用错误。 | 创建自定义错误对象,或者在不适用其他特定错误类型时使用。 |
InternalError | 表示 JavaScript 引擎内部错误,通常是内存问题或资源耗尽导致的。 | 较少在常规代码中直接遇到,通常与实现相关。 |
EvalError | 与 eval() 函数相关的错误。 | 通常在禁用 eval() 时使用,但现代 JavaScript 很少使用。 |
RangeError | 数值超出其允许范围时抛出的错误,例如尝试创建长度为负数的数组。 | 常见于数值计算或数组操作。 |
ReferenceError | 引用一个不存在的变量时抛出的错误。 | 常见于访问未定义的变量或对象属性时。 |
SyntaxError | 代码中存在语法错误时抛出的错误。 | 常见于代码解析阶段,例如 JSON.parse() 中的错误。 |
TypeError | 变量或参数不是预期类型时抛出的错误。 | 常见于类型检查失败或对 null 或 undefined 进行操作时。 |
URIError | 全局 URI 处理函数(如 decodeURI )中使用不正确的 URI 时抛出的错误。 | 常见于 URI 格式无效或编码错误时。 |
用法
当 try/catch
中发生错误时,浏览器会认为错误被处理了,因此就不会再使用本章前面提到的机制报告错误。如果应用程序的用户不懂技术,那么他们即使看到错误也看不懂,这是一个理想的结果。 使用 try/catch
可以针对特定错误类型实现自定义的错误处理。
try/catch
语句最好用在自己无法控制的错误上。例如,假设你的代码中使用了一个大型 JavaScript 库的某个函数,而该函数可能会有意或由于出错而抛出错误。因为不能修改这个库的代码,所以为防止这个函数报告错误,就有必要通过 try/catch 语句把该函数调用包装起来,对可能的错误进行处理。
如果你明确知道自己的代码会发生某种错误,那么就不适合使用 try/catch
语句。例如,如果给函数传入字符串而不是数值时就会失败,就应该检查该函数的参数类型并采取相应的操作。这种情况下, 没有必要使用 try/catch
语句。
抛出错误
与 try/catch
语句对应的一个机制是 throw
操作符,用于在任何时候抛出自定义错误。throw
操作符必须有一个值,但值的类型不限。下面这些代码都是有效的:
throw 12345;
throw 'Hello world!';
throw true;
throw { name: 'JavaScript' };
使用 throw
操作符时,代码立即停止执行,除非 try/catch
语句捕获了抛出的值。
可以通过内置的错误类型来模拟浏览器错误。每种错误类型的构造函数都只接收一个参数,就是错误消息。
throw new Error('error message');
自定义错误常用的错误类型是 Error
、RangeError
、ReferenceError
、TypeError
和其余4种错误类型:
throw new Error('error message');
throw new SyntaxError("I don't like your syntax.");
// InternalError 不是所有 JavaScript 环境中的标准错误类型,因此它可能不存在于你使用的环境中。
// throw new InternalError("I can't do that, Dave.");
throw new TypeError('What type of variable do you take me for?');
throw new RangeError("Sorry, you just don't have the range.");
throw new EvalError("That doesn't evaluate.");
throw new URIError('Uri, is that you?');
throw new ReferenceError("You didn't cite your references properly.");
自定义错误类型
如果你需要创建 一个自定义的错误类型,可以继承自 Error 类来实现:
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'CustomError';
}
}
throw new CustomError("I can't do that, Dave.");
何时抛出错误
抛出自定义错误是解释函数为什么失败的有效方式。在出现已知函数无法正确执行的情况时就应该抛出错误。换句话说,浏览器会在给定条件下执行该函数时抛出错误。例如,下面的函数会在参数不是数组时抛出错误:
function process(values) {
values.sort();
for (let value of values) {
if (value > 100) {
return value;
}
}
return -1;
}
如果给这个函数传入字符串,调用 sort()函数就会失败。每种浏览器对此都会给出一个模棱两可的错误消息, 如 Chrome:对象名没有方法'sort';Firefox: values.sort() 不是函数。
如果是一个复杂 的 Web 应用程序,有几千行 JavaScript 代码,想要找到错误的原因就会很难。这时候,使用适当的信息创建自定义错误可以有效提高代码的可维护性。
function process(values) {
if (!(values instanceof Array)) {
throw new Error('process(): Argument must be an array.');
}
values.sort();
for (let value of values) {
if (value > 100) {
return value;
}
}
return -1;
}
抛出错误与 try/catch
一个常见的问题是何时抛出错误,何 时使用 try/catch 捕获错误。一般来说,错误要在应用程序架构的底层抛出,在这个层面上,人们对正在进行的流程知之甚少,因此无法真正地处理错误。如果你在编写一个可能用于很多应用程序的 JavaScript 库,或者一个会在应用程序的很多地方用到的实用函数, 那么应该认真考虑抛出带有详细信息的错误。然后捕获和处理错误交给应用程序就行了。
至于抛出错误与捕获错误的区别,可以这样想:应该只在确切知道接下来该做什么的时候捕获错误。捕获错误的目的是阻止浏览器以其默认方式响应;抛出错误的目的是为错误提供有关其发生原因的说明。
error 事件
任何没有被 try/catch 语句处理的错误都会在 window 对象上触发 error 事件。该事件是浏览器早期支持的事件,为保持向后兼容,很多浏览器保持了其格式不变。在 onerror 事件处理程序中,任何浏览器都不会传入 event 对象。相反,会传入 3 个参数:错误消息、发生错误的 URL 和行号。大多数情况下,只有错误消息有用,因为 URL 就是当前文档的地址,而行号可能指嵌入 JavaScript 或外部文件中的代码。另外,onerror 事件处理程序需要使用 DOM Level 0 技术来指定,因为它不遵循 DOM Level 2 Events 标准格式:
window.onerror = (message, url, line) => {
console.log(message);
};
在任何错误发生时,无论是否是浏览器生成的,都会触发 error 事件并执行这个事件处理程序。 然后,浏览器的默认行为就会生效,像往常一样显示这条错误消息。可以返回 false 来阻止浏览器默认报告错误的行为,如下所示:
window.onerror = (message, url, line) => {
console.log(message);
return false;
};
通过返回 false,这个函数实际上就变成了整个文档的 try/catch 语句,可以捕获所有未处理的 27 运行时错误。这个事件处理程序应该是处理浏览器报告错误的最后一道防线。理想情况下,最好永远不要用到。适当使用 try/catch 语句意味着不会有错误到达浏览器这个层次,因此也就不会触发 error 事件。
图片也支持 error 事件。任何时候,如果图片 src 属性中的 URL 没有返回可识别的图片格式,就会触发 error 事件。这个事件遵循 DOM 格式,返回一个以图片为目标的 event 对象。
const image = new Image();
image.addEventListener('load', event => {
console.log('Image loaded!');
});
image.addEventListener('error', event => {
console.log('Image not loaded!');
});
image.src = 'doesnotexist.gif'; // 不存在,资源会加载失败
错误处理策略
过去,Web 应用程序的错误处理策略基本上是在服务器上落地。错误处理策略涉及很多错误和错误处理考量,包括日志记录和监控系统。这些主要是为了分析模式,以期找到问题的根源并了解有多少用户会受错误影响。
在 Web 应用程序的 JavaScipt 层面落地错误处理策略同样重要。因为任何 JavaScript 错误都可能导致网页无法使用,所以理解这些错误会在什么情况下发生以及为什么会发生非常重要。绝大多数 Web 应用程序的用户不懂技术,在碰到页面出问题时通常会迷惑。为解决问题,他们可能会尝试刷新页面,也可能会直接放弃。作为开发者,应该非常清楚自己的代码在什么情况下会失败,以及失败会导致什么结果。另外,还要有一个系统跟踪这些问题。
识别错误
错误处理非常重要的部分是首先识别错误可能会在代码中的什么地方发生。因为 JavaScript 是松散类型的,不会验证函数参数,所以很多错误只有在代码真正运行起来时才会出现。通常,需要注意 3 类错误:
- 类型转换错误
- 数据类型错误
- 通信错误
上面这几种错误会在特定情况下,在没有对值进行充分检测时发生。