错误处理与调试
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;
}