21.1 浏览器错误报告

21.1.1 桌面控制台

所有现代桌面浏览器都会通过控制台暴露错误。
这些错误可以显示在开发者工具内嵌的控制台中。

21.1.2 移动控制台

移动浏览器不会直接在设备上提供控制台界面。不过,还是有一些途径可以在移动设备中检查错误。
Chrome移动版和Safari的iOS版内置了实用工具,支持将设备连接到宿主操作系统中相同的浏览器。然后,就可以在对应的桌面浏览器中查看错误了。这涉及设备之间的硬件连接,且要遵循不同的操作步骤。
此外也可以使用第三方工具直接在移动设备上调试。

21.2 错误处理

21.2.1 try/catch语句

ECMA-262第3版新增了try/catch语句,作为在JavaScript中处理异常的一种方式。
基本的语法如下所示,跟Java中的try/catch语句一样:

  1. try {
  2. // 可能出错的代码
  3. } catch (error) {
  4. // 出错时要做什么
  5. }

任何可能出错的代码都应该放到try块中,而处理错误的代码则放在catch块中。
如下所示:

  1. try {
  2. window.someFunction();
  3. } catch (error) {
  4. console.log('发生了一个错误!')
  5. }

如果try块中有代码发生错误,代码会立即退出执行,并跳到catch块中。
catch块此时接收到一个对象,该对象包含发生错误的相关信息。
与其他语言不同,即使在catch块中不使用错误对象,也必须为它定义名称。
错误对象中暴露的实际信息因浏览器而异,但至少包含保存错误消息的message属性
ECMA-262也指定了定义错误类型的name属性,目前所有浏览器中都有这个属性。

1.finally子句

try/catch语句中可选的finally子句始终运行。
如果try块中的代码运行完,则接着执行finally块中的代码。
如果出错并执行catch块中的代码,则finally块中的代码仍执行。
try或catch块无法阻止finally块执行,包括return语句。
比如:

  1. function testFinally() {
  2. try {
  3. return 2;
  4. } catch (error) {
  5. return 1;
  6. } finally {
  7. return 0;
  8. }
  9. }

注:只要代码中包含了finally子句,try块或catch块中的return语句就会被忽略。

2.错误类型

代码执行过程中会发生各种类型的错误。
每种类型都会对应一个错误发生时抛出的错误对象。
ECMA-262定义了以下8种错误类型:
❑ Error ❑ InternalError ❑ EvalError ❑ RangeError
❑ ReferenceError ❑ SyntaxError ❑ TypeError ❑ URIError
Error是基类型,其他错误类型继承该类型。因此,所有错误类型都共享相同的属性(所有错误对象上的方法都是这个默认类型定义的方法)。
InternalError类型的错误会在底层JavaScript引擎抛出异常时由浏览器抛出。例如,递归过多导致了栈溢出。
EvalError类型的错误会在使用eval()函数发生异常时抛出。ECMA-262规定,“如果eval属性没有被直接调用(即没有将其名称作为一个Identifier,也就是CallExpression中的MemberExpression),或者如果eval属性被赋值”,就会抛出该错误。基本上,只要不把eval()当成函数调用就会报告该错误。
RangeError错误会在数值越界时抛出。例如,定义数组时如果设置了并不支持的长度,如-20或Number.MAX_VALUE,就会报告该错误。
RangeError在JavaScript中发生得不多。
ReferenceError会在找不到对象时发生。(这就是著名的”objectexpected”浏览器错误的原因。)这种错误经常由访问不存在的变量而导致
SyntaxError经常在给eval()传入的字符串包含JavaScript语法错误时发生。
TypeError在JavaScript中很常见,主要发生在变量不是预期类型,或者访问不存在的方法时。很多原因可能导致这种错误,尤其是在使用类型特定的操作而变量类型不对时。
URIError,只会在使用encodeURI()或decodeURI()但传入了格式错误的URI时发生。这个错误恐怕是JavaScript中难得一见的错误了,因为上面这两个函数非常稳健。
在try/catch语句的catch块中,可以使用instanceof操作符确定错误的类型。

3.try/catch的用法

当try/catch中发生错误时,浏览器会认为错误被处理了,因此就不会再使用本章前面提到的机制报告错误。
使用try/catch可针对特定错误类型实现自定义的错误处理。try/catch语句最好用在自己无法控制的错误上。
如果你明确知道自己的代码会发生某种错误,那就不适合使用try/catch语句。

21.2.2 抛出错误

与try/catch语句对应的一个机制是throw操作符,用于在任何时候抛出自定义错误。
throw操作符必须有一个值,但值的类型不限。
使用throw操作符时,代码立即停止执行,除非try/catch语句捕获了抛出的值。

1.何时抛出错误

抛出自定义错误是解释函数为什么失败的有效方式。
在出现已知函数无法正确执行的情况时就应该抛出错误。
换句话说,浏览器会在给定条件下执行该函数时抛出错误。

  1. function process(values) {
  2. if (!(values instanceof Array)) {
  3. throw new Error('process(): 参数必须是数组');
  4. }
  5. values.sort();
  6. for (let value of values) {
  7. if (value > 100) {
  8. return value;
  9. }
  10. }
  11. return -1;
  12. }

若没有提示错误信息的语句,则:
如果给这个函数传入字符串,调用sort()函数就会失败。每种浏览器对此都会给出一个模棱两可的错误消息,如下所示:
❑ IE:属性或方法不存在。
❑ Firefox:values.sort()不是函数。
❑ Safari:值undefined(对表达式values.sort求值的结果)不是一个对象。
❑ Chrome:对象名没有方法’sort’。❑ Opera:类型不匹配(通常是在需要对象时使用了非对象值)。
使用适当的信息创建自定义错误可以有效提高代码的可维护性。

2.抛出错误与try/catch

一个常见的问题是何时抛出错误,何时使用try/catch捕获错误。
至于抛出错误与捕获错误的区别,可以这样想:
应该只在确切知道接下来该做什么的时候捕获错误。
捕获错误的目的是:阻止浏览器以其默认方式响应;
抛出错误的目的是:为错误提供有关其发生原因的说明。

21.2.3 error事件

任何没有被try/catch语句处理的错误都会在window对象上触发error事件。
在onerror事件处理程序中,任何浏览器都不会传入event对象。
相反,会传入3个参数:错误消息、发生错误的URL和行号。
大多数情况下,只有错误消息有用,因为URL就是当前文档的地址,而行号可能指嵌入JavaScript或外部文件中的代码。
在任何错误发生时,无论是否是浏览器生成的,都会触发error事件并执行这个事件处理程序。
然后,浏览器的默认行为就会生效,像往常一样显示这条错误消息。可以返回false来阻止浏览器默认报告错误的行为。

21.2.4 错误处理策略

错误处理策略涉及很多错误和错误处理考量,包括日志记录和监控系统。
这些主要是为了分析模式,以期找到问题的根源并了解有多少用户会受错误影响。
作为开发者,应该非常清楚自己的代码在什么情况下会失败,以及失败会导致什么结果。
另外,还要有一个系统跟踪这些问题。

21.2.5 识别错误

错误处理非常重要的部分是:首先识别错误可能会在代码中的什么地方发生。
因为JavaScript是松散类型的,不会验证函数参数,所以很多错误只有在代码真正运行起来时才会出现。
通常,需要注意3类错误:
❑ 类型转换错误 ❑ 数据类型错误 ❑ 通信错误
上面这几种错误会在特定情况下,在没有对值进行充分检测时发生。

1.静态代码分析器

通过在代码构建流程中添加静态代码分析或代码检查器(linter),可以预先发现非常多的错误。
这样的代码分析工具有很多,详见GitHub Gist网站All Gists页面。
常用的静态分析工具是JSHint、JSLint、Google Closure和TypeScript。
静态代码分析器要求使用类型、函数签名及其他指令,来注解JavaScript,以此描述程序如何在基本可执行代码之外运行。
分析器会比较注解和JavaScript代码的各个部分,对在实际运行时可能出现的潜在不兼容问题给出提醒。

2.类型转换错误

类型转换错误的主要原因是:使用了会自动改变某个值的数据类型的操作符或语言构造。
使用等于(==)或不等于(! =)操作符,以及在if、for或while等流控制语句中使用非布尔值,经常会导致类型转换错误。
大多数情况下,最好使用严格相等(===)和严格不相等(! ==)操作符来避免类型转换。
类型转换错误也会发生在流控制语句中。
比如,if语句会自动把条件表达式转换为布尔值,然后再决定下一步的走向。
在实践中,if语句是问题比较多的。
来看下面的例子:

  1. function concat(str1, str2, str3) {
  2. let result = str1 + str2;
  3. if (str3) { // 不要这样判断!
  4. result += str3;
  5. }
  6. return result;
  7. }

命名变量如果没有被赋值就会自动被赋予undefined值。而在默认转换中,undefined会被转换为布尔值false。因此这个函数的用意是在提供了第三个参数的情况下,才会在拼接时带上它。
问题在于并非只有undefined会转换为false,字符串也不是唯一可转换为true的值。假如第三个参数是数值0, if条件判断就会失败,而数值1则会导致满足条件。
在流控制语句中使用非布尔值作为条件是很常见的错误来源。
为避免这类错误,需要始终坚持使用布尔值作为条件。这通常可以借助某种比较来实现。
例如,可以把前面的函数改写为如下形式:

  1. function concat(str1, str2, str3) {
  2. let result = str1 + str2;
  3. if (typeof(str3) === 'String') { // 恰当地比较
  4. result += str3;
  5. }
  6. return result;
  7. }

3.数据类型错误

因为JavaScript是松散类型的,所以变量和函数参数都不能保证会使用正确的数据类型。
开发者需要自己检查数据类型,确保不会发生错误。
数据类型错误常发生在将意外值传给函数的时候。

4.通信错误

随着Ajax编程的出现,Web应用程序在运行期间动态加载数据和功能成为常见的情形。
JavaScript和服务器之间的通信也会出现错误。
第一种错误是URL格式或发送数据的格式不正确。
通常,在把数据发送到服务器之前没有用encodeURIComponent()编码,会导致这种错误。
在服务器响应非预期值时也会发生通信错误。在动态加载脚本或样式时,请求的资源有可能不可用。

21.2.6 区分重大与非重大错误

任何错误处理策略中一个非常重要的方面就是:确定某个错误是否为重大错误。
具有以下一个或多个特性的错误属于非重大错误:
❑ 不会影响用户的主要任务;
❑ 只会影响页面中某个部分;
❑ 可以恢复;
❑ 重复操作可能成功。
另一方面,重大错误具备如下特性:
❑ 应用程序绝对无法继续运行;
❑ 错误严重影响了用户的主要目标;
❑ 会导致其他错误发生。
非重大错误和重大错误的区别主要体现在:对用户的影响上。
好的代码设计意味着应用程序某个部分的错误不会影响其他部分,实际上根本不应该相关。

21.2.7 把错误记录到服务器中

Web应用程序开发中的一个常见做法是:建立中心化的错误日志存储和跟踪系统。
数据库和服务器错误正常写到日志中并按照常用API加以分类。
对复杂的Web应用程序而言,最好也把JavaScript错误发送回服务器记录下来。
这样做可以把错误记录到与服务器相同的系统,只要把它们归类到前端错误即可。
使用相同的系统可以进行相同的分析,而不用考虑错误来源。