在前端的 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
属性,在错误栈中会展示该属性值,如果用于命名重要的业务逻辑属性,将有效帮助排查错误。
Fetch API
在使用 Fetch API 来进行数据请求时,普通的异常信息如状态码、状态结果信息等会包含在 Fetch 的 Response 对象中。
fetch('/data').then((response) => {
if (!res.ok) {
console.log('status', response.status);
console.log('statusText', response.statusText);
console.log('url', response.url);
}
});
如在请求的时候发生 500 错误,那么 status
的值就是 500,而 statusText
的值就是 Internal Server Error
。
上面说的是后端服务器能响应的情况下返回的异常信息,如果服务器没有响应,就要使用 catch 方法来进行错误捕获,此时错误信息会统一返回 TypeError: Failed to fetch
。
fetch('/data').then((response) => {
if (!res.ok) {
console.log('status', response.status);
console.log('statusText', response.statusText);
console.log('url', response.url);
}
})
.catch((error) => {
console.log('catch the error');
console.log(error); // => TypeError: Failed to fetch
});
请求错误进入 catch 语句块的关键点在于 服务器没有响应。
服务器返回了 404、500 等错误状态码是服务器有响应,但是响应出现了异常,返回的不是预期的结果。
在请求和响应的过程中发生了网络错误,此时才意味着服务器没有响应。比如浏览器端发起的请求需要经过 Nginx 的反向代理才达到实际的 Node.js 的服务器,那么此时 Node.js 服务在未运行的情况下会正常的返回 502 错误,这个返回是 Nginx 服务的返回,Fetch 时不会进入 catch 语句块。如果没有 Nginx,而是直接请求 Node.js 的服务器,如果 Node.js 服务在未运行的情况下,此时是没有任何服务器能响应这个请求的,在这种场景下就会进入到 catch 语句块。
如果在请求发生异常的时候发生了跨域,此时也会进入到 catch 语句块,如果要正确的捕获到异常需要在服务端正确的设置 CORS。
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
通过对错误信息的二次封装,可以增加一些有利于快速定位错误的额外信息。
2019.12.11 新增了 Fetch API 部分。
本文基于 JavaScript Errors Handbook 做了实际的测试,对内容进行了删减和简化,有兴趣的可以前往阅读。