# 错误处理

错误处理在程序设计中相当重要。

任何有影响力的 Web 应用程序都需要一套完善的错误处理机制,同时考虑类型、频率,或者其他重要的标准对错误进行分类。这样,开发人员才能解决应用程序可能会出现的问题。

# try-catch 语句

try-catch 语句是 JavaScript 中处理异常的一种标准方式,我们把所有可能会抛出错误的代码都放在 try 语句块中,然后把那些用于错误处理的代码放在 catch 块中。

try {
  // do someing
} catch (error) {
  // 处理错误
}

如果 try 块中的任何代码发生了错误,就会立即执行 catch 块中的代码,catch 块会接收到一个包含错误信息的对象。

与在其他语言中不同的是,即使不使用这个错误对象,也要给它起个名字。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的 message 属性。ECMA-262 还规定了一个保存错误类型的 name 属性。

# finally 子句

finally 子句在 try-catch 语句中是可选的,但 finally 子句一经使用,其代码无论如何都会执行。无论 trycatch 语句块中包含什么代码——甚至 return 语句,都不能阻止。

function testFinally() {
  try {
    console.log(NonexistentVariable)
  } catch (error) {
    return error
  } finally {
    console.log('something')
  }
}

另外,如果提供 finally 子句,则 catch 子句就成了可选的(catch 或 finally 有一个即可)。

# 错误类型

CMA-262 定义了下列 7 种错误类型:

  • Error
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

其中,Error 是基类型,其他错误类型都继承自该类型。Error 类型的错误很少见,如果有也是浏览器抛出的,这个基类型的主要目的是供开发人员抛出自定义错误。

EvalError 类型的错误会在使用 eval() 函数而发生异常时被抛出:如果没有把 eval() 当成函数调用,就会抛出错误。

new eval() //抛出EvalError
eval = foo //抛出EvalError

RangeError 类型的错误会在数值超出相应范围时触发。

var items1 = new Array(-20) //抛出RangeError
var items2 = new Array(Number.MAX_VALUE) //抛出RangeError

ReferenceError 在找不到对象的情况下("object expected" 浏览器错误)便会发生。

var obj = x //在 x 并未声明的情况下抛出 ReferenceError

SyntaxError 在把语法错误的 JavaScript 字符串传入 eval() 函数时就会出现。

eval('a ++ b') //抛出SyntaxError

TypeError 类型在 JavaScript 中会经常用到,在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。

var o = new 10()

URIError 错误在使用 encodeURI()decodeURI() 时,而 URI 格式不正确时,就会导致。

要想知道错误的类型,可以在 try-catch 语句的 catch 语句中使用 instanceof 操作符。

# 合理使用 try-catch

try-catch 语句中发生错误时,浏览器会认为错误已经被处理了,因而不会记录或报告错误,以能够让我们实现自己的错误处理机制。

使用 try-catch 最适合处理那些我们无法控制的错误。

比如,我们在调用第三方库的方法时,如果有意无意地抛出一些错误,由于我们不能修改这个库的源代码,所以大可将对该函数的调用放在 try-catch 语句当中,这样就可以恰当地处理它们。

在明明白白地知道自己的代码会发生错误时,再使用 try-catch 语句就不需要这样多此一举了。

# 抛出错误

try-catch 语句相配的还有一个 throw 操作符,用于随时抛出自定义错误。抛出错误时,必须要给 throw 操作符指定一个值,这个值是什么类型,没有要求。

在遇到 throw 操作符时,代码会立即停止执行。仅当有 try-catch 语句捕获到被抛出的值时,代码才会继续执行。

通过使用某种内置错误类型,可以更真实地模拟浏览器错误。每种错误类型的构造函数接收一个参数,即实际的错误消息。

在创建自定义错误消息时最常用的错误类型是 ErrorRangeErrorReferenceErrorTypeError

另外,利用原型链还可以通过继承 Error 来创建自定义错误类型。

function CustomError(message) {
  this.name = 'CustomError'
  this.message = message
}

CustomError.prototype = new Error()
throw new CustomError('Error message')

浏览器对待继承自 Error 的自定义错误类型,就像对待其他错误类型一样。如果要捕获自己抛出的错误并且把它与浏览器错误区别对待的话,创建自定义错误是很有用的。

TIP

  • 通常,应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。
  • 捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。

# 错误(error)事件

任何没有通过 try-catch 处理的错误都会触发 window 对象的 error 事件。onerror 事件处理程序都不会创建 event 对但它可以接收三个参数:错误消息、错误所在的 URL 和行号。

只要发生错误,无论是不是浏览器生成的,都会触发 error 事件并执行这个事件处理程序。然后,浏览器像往常一样显示出错误消息。如果在事件处理程序中返回 false,就可以阻止浏览器报告错误的默认行为。

window.onerror = function(message, url, line) {
  console.log(message)
  return false
}

图像也支持 error 事件,只要图像的 src 特性中的 URL 不能返回可以被识别的图像格式,就会触发 error 事件。此时的 error 事件遵循 DOM 格式,会返回一个以图像为目标的 event 对象。

var image = new Image()
image.onload = function() {}
image.onerror = function() {}
image.src = 'doraemon.png'

当加载图像失败时就会执行错误处理程序。需要注意的是,发生 error 事件时,图像下载过程已经结束,也就是说不能再重新下载了。

# 常见的错误类型

  • 类型转换错误

类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。

容易发生类型转换错误的另一个地方,就是流控制语句。像 if 之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。尤其是 if 语句,如果使用不当,最容易出错。

function concat(str1, str2, str3) {
  var result = str1 + str2
  if (str3) {
    // 绝对不要这样,建议使用:'string' === typeof str3
    result += str3
  }
  return result
}
  • 数据类型错误

JavaScript 是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。

比如,在一些函数中,对传入的参数没有进行类型检测,然后调用数组具有的一些方法做一些业务上的处理。当传入的参数是字符串时,那么对其调用数组特有的方法就会报错。

  • 通信错误

最常见的问题是在将数据发送给服务器之前,没有使用 encodeURIComponent() 对数据进行编码,对于查询字符串,应该记住必须要使用 encodeURIComponent() 方法。

# 调试技术

  • 将消息记录到当前页面

在页面中开辟一小块区域,用以显示消息。这个区域通常是一个元素,而该元素可以总是出现在页面中,但仅用于调试目的。

function log(message) {
  var console = document.getElementById('debuginfo')
  if (console === null) {
    console = document.createElement('div')
    console.id = 'debuginfo'
    console.style.background = '#dedede'
    console.style.border = '1px solid silver'
    console.style.padding = '5px'
    console.style.width = '400px'
    console.style.position = 'absolute'
    console.style.right = '0px'
    console.style.top = '0px'
    document.body.appendChild(console)
  }
  console.innerHTML += '<p>' + message + '</p>'
}

这种技术在不支持 JavaScript 控制台的 IE7 及更早版本或其他浏览器中十分有用,它先检测是否已经存在调试元素,如果没有则会新创建一个 <div> 元素,并为该元素应用一些样式,以便与页面中的其他元素区别开。然后,又使用 innerHTML 将消息写入到这个 <div> 元素中。结果就是页面中会有一小块区域显示错误消息。

  • 将消息记录到控制台

通过 console 对象向 JavaScript 控制台中写入消息,这个对象具有下列方法:

  • error(message):将错误消息记录到控制台。
  • info(message):将信息性消息记录到控制台。
  • log(message):将一般消息记录到控制台。
  • warn(message):将警告消息记录到控制台。

错误消息带有红色图标,而警告消息带有黄色图标。

  • 抛出错误

抛出错误也是一种调试代码的好办法,自定义的错误通常都使用 assert() 函数抛出。这个函数接受两个参数,一个是求值结果应该为 true 的条件,另一个是条件为 false 时要抛出的错误。

function assert(condition, message) {
  if (!conditin) throw new Error(message)
}

// 使用方式
function divide(dividend, divisor) {
  assert(
    typeof dividend == 'number' && typeof divisor == 'number',
    'divide(): Both arguments must be numbers.',
  )

  return dividend / divisor
}

可见,使用 assert() 函数可以减少抛出错误所需的代码量,而且也比前面的代码更容易看懂。

# 参考资料

  • JavaScript 高级程序设计(第 3 版)