在前端的 JavaScript 开发中,发现开发者对于错误异常的处理普遍都比较简单粗暴,如果应用程序中缺少有效的错误处理和容错机制,代码的健壮性就无从谈起。本文整理出了一些常见的错误异常处理的场景,旨在为前端的 JavaScript 错误异常处理提供一些基础的指导。
Error 对象
先来简单介绍一下 JavaScript 中的 Error 对象,通常 Error 对象由重要的两部分组成,包含了 error.message
错误信息和 error.stack
错误追溯栈。
产生一个错误很简单,比如在 foo.js
中直接调用一个不存在的 callback
函数。
// foo.js
function foo () {
callback();
}
foo();
此时通过 Chrome 浏览器的控制台会展示如下的信息。
Uncaught ReferenceError: callback is not defined
at foo (foo.js:2)
at foo.js:5
其中 Uncaught ReferenceError: callback is not defined
就是 error.message
错误信息,而剩下的 at xxx 就是具体的错误追溯栈,在 Chrome 的控制台中,对错误的展示进行了优化。
如果我们通过 window.onerror
来捕获到该错误后将 Error 对象直接输出到页面中会展示出更原始的数据。
<!-- 展示错误的容器 -->
<textarea id="error"></textarea>
// 输出错误
window.onerror = function (msg, url, line, col, err) {
document.getElementById('error').textContent = err.message + '\n\n' + err.stack;
};
原始的错误数据中会展示出错误追溯栈中的 Source URL。
callback is not defined
ReferenceError: callback is not defined
at foo (http://example.com/js-error/foo.js:2:5)
at http://example.com/js-error/foo.js:5:1
有了错误追溯栈,就能通过发生错误的文件 Source URL 和错误在代码中的具体位置来快速定位到错误。
看起来好像很简单,但实际的开发中如何有效的捕获错误,如何有效的抛出错误都有一些需要注意的点,下面逐个的来讲解。
window.onerror
前端在捕获错误时都会通过绑定 window.onerror
事件来捕获全局的 JavaScript 执行错误,标准的浏览器在响应该事件时会依次提供 5 个参数。
window.onerror = function(message, source, lineno, colno, error) { ... }
- message 错误信息
- source 错误发生时的页面 URL
- lineno 错误发生时的 JS 文件行数
- colno 错误发生时的 JS 文件列数
- error 错误发生时抛出的标准 Error 对象
使用 window.addEventListener
也能绑定 error
事件,但是该事件函数的参数是一个 ErrorEvent 对象。
绑定 window.onerror
事件时,事件处理函数的第 5 个参数在低版本浏览中或 JS 资源跨域场景下可能不是 Error 对象。
在 Chrome 浏览器中如果页面加载的 JS 资源文件中存在跨域的 script 标签,在发生错误时会提示 Script error
而缺乏错误追溯栈。
window.onerror
在响应跨域 JavaScript 错误时缺乏错误追溯栈时的 arguments
对象如下:
[
'Script error.',
'',
0,
0,
null
]
为了正常的捕获到跨域 JS 资源文件的错误,需要具备两个条件: 1. 为 JS 资源文件增加 CORS 响应头。 2. 通过 script 引用该 JS 文件时增加 crossorigin="anonymous"
的属性,如果是动态加载的 JS,可以写作 script.crossOrigin = true
。
window.onerror
能捕获一些全局的 JavaScript 错误,但还有不少场景在全局是捕获不到的。
try/catch
window.onerror
能捕获全局场景下的错误,如果已知一些程序的场景中可能会出现错误,这个时候一般会使用 try/catch
来进行捕获。
但是在使用 try/catch
块时无法捕获异步错误,例如块中使用了 setTimeout
。
try {
setTimeout(function () {
callTimeout(); // callTimeout 未定义,会抛错
}, 1000);
}
catch (err) {
console.log('catch the error', err); // 不会被执行
}
try/catch
在处理 setTimeout
这类异步场景时是无效的,执行时仍会抛错,catch 中的代码不会被执行。
虽然在 try/catch
中没有捕获到,此时如果有绑定 window.onerror
则会被全局捕获。
由此可见, try/catch
应该是只能捕获 JS Event Loop 中同步的任务。
如果想正确的捕获 setTimeout
中的错误,需要将 try/catch
块写到 setTimeout
的函数中。
setTimeout(function () {
try {
callTimeout(); // callTimeout 未定义,不会抛错
}
catch (err) {
console.log('catch the error', err); // 将会被执行
}
}, 1000);
Promise
Promise 有自己的错误处理机制,通常 Promise 函数中的错误无法被全局捕获。
var promise = new Promise(executor);
promise.then(onFulfilled, onRejected);
比较容易遗漏错误处理的地方有 executor
和 onFulfilled
,在这些函数中如果发生错误都不能被全局捕获。
正确的捕获 Promise 的错误,应该使用 Promise.prototype.catch
方法,意外的错误和使用 reject 主动捕获的错误都会触发 catch 方法。
catch 方法中通常会接收到一个 Error 对象,但是当调用 reject 函数时传入的是一个非 Error 对象时,catch 方法也会接收到一个非 Error 对象,这里的 reject 和 throw 的表现是一样的,所以在使用 reject 时,最好是传入一个 Error 对象。
reject(
new Error('this is reject message')
);
值得注意的是,如果 Promise 的 executor
中存在 setTimeout
语句时, setTimeout
的报错会被全局捕获。
Async Function
Async Function 和 Promise 一样,发生错误不会被全局的 window.onerror
捕获,所以在使用时如果有报错,需要手动增加 try/catch
语句。
匿名函数
匿名函数的使用在 JavaScript 中很常见,但是当出现匿名函数的报错时,在错误追溯栈中会以 anonymous
来标识错误,为了排查错误方便,可以将函数进行命名,或者使用函数的 displayName
属性。
函数如果有 displayName
属性,在错误栈中会展示该属性值,如果用于命名重要的业务逻辑属性,将有效帮助排查错误。
throw error
上面说了很多错误捕获的注意点,如果要主动的抛错,都会使用 throw
来抛错,常见的几种抛错方法如下:
throw new Error('Problem description.') // 方法 1
throw Error('Problem description.') // 方法 2
throw 'Problem description.' // 方法 3
throw null // 方法 4
其中方法 1 和方法 2 的效果一样,浏览器都能正确的展示错误追溯栈。方法 3 和方法 4 不推荐,虽然能抛错,但是在抛错的时候不能展示错误追溯栈。
try/catch
和 throw
,一个用来捕获错误,一个用来抛出错误,如果两个结合起来用通常等于脱了裤子放屁多此一举,唯一有点用的是可以对错误信息进行再加工。
可以在 Chrome 控制台中模拟出一个结合使用的实际场景。
try {
foo();
}
catch (err) {
err.message = 'Catch the error: ' + err.message;
throw Error(err);
}
由于在 catch 块中又抛出了错误,所以该错误没有被捕获到,但此时错误信息经过了二次封装。
Uncaught Error: ReferenceError: Catch the error: foo is not defined
通过对错误信息的二次封装,可以增加一些有利于快速定位错误的额外信息。