js的错误处理与调试

错误处理在编程中的重要性毋庸置疑。所有主流 Web 应用程序都需要定义完善的错误处理协议,大多数优秀的应用程序都有自己的错误处理策略,尽管主要逻辑是放在服务端的。事实上,服务器团队通常会花很多精力根据错误类型、频率和其他重要指标来定义规范的错误日志机制。最终实现通过简单的数据库查询就可以了解应用程序的运行状态。

try/catch

try/catch 语句,作为在 js 中处理异常的一种方式。基本语法如下所示:

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

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

1
2
3
4
5
try {
window.someNonexistenFunction()
} catch (error) {
console.log('An error happened')
}

错误对象中暴露的实际信息因浏览器而异,但是至少包含保存错误消息的 message 属性,所以可以像下面的代码这样必要时显示错误消息:

1
2
3
4
5
try {
window.someNonexistenFunction()
} catch (error) {
console.log(error.message)
}

finally 子句

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

finally 块的存在会导致 try 块中的 return 语句被忽略,因此无论什么时候调用该函数都会返回 0。如果去掉了 finally 子句,那么该函数会返回 2.如果写出了 finally 子句,那么 catch 块就变成了可选的。

finally 子句会让 try 块或者 catch 块中的 return 语句被忽略。

错误类型

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

Error 是基类型,其他错误类型继承该类型。

InternalError 类型的错误会在底层 JavaScript 引擎抛出异常时由浏览器抛出。

EvalError 类型的错误会在使用 eval()函数发生异常时抛出。

RangeError 错误会在数值越界时抛出。

ReferenceError 会在找不到对象时发生。

SyntaxError 经常在给 eval()传入的字符串包含 JavaScript 语法错误时发生。

TypeError 在 JavaScript 中很常见,主要发生在变量不是预期类型,或者访问不存在的方法时。

最后一种错误类型是 URIError,只会在使用 encodeURI()或 decodeURI()但传入了格式错误的 URI 时发生。

不同的错误类型可用于为异常提供更多信息,以便实现适当的错误处理逻辑。在 try/catch 语句的 catch 块中,可以使用 instanceof 操作符确定错误的类型,比如:

1
2
3
4
5
6
7
8
9
10
11
try {
someFunction()
} catch (error) {
if (error instanceof TypeError) {
// 处理类型错误
} else if (error instanceof ReferenceError) {
// 处理引用错误
} else {
// 处理所有其他类型的错误
}
}

try/catch 的用法

try/catch 语句最好用在自己无法控制的错误上。假设你的代码中使用了一个大型 JavaScript 库的某个函数,而该函数可能会有意或由于出错而抛出错误。因为不能修改这个库的代码,所以为防止这个函数报告错误,就有必要通过 try/catch 语句把该函数调用包装起来,对可能的错误进行处理。

如果你明确知道自己的代码会发生某种错误,那么就不适合使用 try/catch 语句。例如,如果给函数传入字符串而不是数值时就会失败,就应该检查该函数的参数类型并采取相应的操作。这种情况下,没有必要使用 try/catch 语句。

抛出错误

与 try/catch 语句对应的一个机制是 throw 操作符,用于在任何时候抛出自定义错误。throw 操作符必须有一个值,但值的类型不限。下面这些代码都是有效的:

1
2
3
4
throw 12345
throw 'Hello world!'
throw true
throw { name: 'JavaScript' }

使用 throw 操作符时,代码立即停止执行,除非 try/catch 语句捕获了抛出的值。

自定义错误常用的错误类型是 Error、RangeError、ReferenceError 和 TypeError。

抛出错误与 try/catch

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

error 事件

任何没有被 try/catch 语句处理的错误都会在 window 对象上触发 error 事件。在 onerror 事件处理程序中,任何浏览器都不会传入 event 对象。相反,会传入 3 个参数:错误消息、发生错误的 URL 和行号。

1
2
3
window.onerror = function (message, url, line) {
console.log(message)
}

在任何错误发生时,无论是否是浏览器生成的,都会触发 error 事件并执行这个事件处理程序。然后,浏览器的默认行为就会生效,像往常一样显示这条错误消息。可以返回 false 来阻止浏览器默认报告错误的行为,如下所示:

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

图片也支持 error 事件。任何时候,如果图片 src 属性中的 URL 没有返回可识别的图片格式,就会触发 error 事件。这个事件遵循 DOM 格式,返回一个以图片为目标的 event 对象。下面是个例子:

1
2
3
4
5
6
7
8
9
10
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 类错误:

  • 类型转换错误
  • 数据类型错误
  • 通信错误

区分重大与非重大错误

任何错误处理策略中一个非常重要的方面就是确定某个错误是否为重大错误。具有以下一个或多个特性的错误属于非重大错误:

  • 不会影响用户的主要任务;
  • 只会影响页面中某个部分;
  • 可以恢复;
  • 重复操作可能成功。

另一方面,重大错误具备如下特性:

  • 应用程序绝对无法继续运行;
  • 错误严重影响了用户的主要目标;
  • 会导致其他错误发生。

把错误记录到服务器中

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

要建立 JavaScript 错误日志系统,首先需要在服务器上有页面或入口可以处理错误数据。该页面只要从查询字符串中取得错误数据,然后把它们保存到错误日志中即可。比如,该页面可以使用如下代码:

1
2
3
4
5
6
7
function logError(sev, msg) {
let img = new Image(),
encodedSev = encodeURIComponent(sev),
encodedMsg = encodeURIComponent(msg)
img.src = 'log.php?sev=${encodedSev}&msg=${encodedMsg}'
}
s

logError()函数接收两个参数:严重程度和错误消息。严重程度可以是数值或字符串,具体取决于使用的日志系统。这里使用 Image 对象发送请求主要是从灵活性方面考虑的。

  • 所有浏览器都支持 Image 对象,即使不支持 XMLHttpRequest 对象也一样。
  • 不受跨域规则限制。
  • 记录错误的过程很少出错。

只要是使用 try/catch 语句的地方,都可以把相关错误记录下来。下面是一个例子:

1
2
3
4
5
6
7
for (let mod of mods) {
try {
mod.init()
} catch (ex) {
logError('nonfatal', 'Module init failed: ${ex.message}')
}
}

在这个例子中,模块初始化失败就会调用 logError()函数。第一个参数是表示错误严重程度的”nonfatal”,第二个参数在上下文信息后面追加了 JavaScript 错误消息。