在前端的 JAVAScript 开发中,发现开发者对于错误异常的处理普遍都比较简单粗暴,如果应用程序中缺少有效的错误处理和容错机制,代码的健壮性就无从谈起。本文整理出了一些常见的错误异常处理的场景,旨在为前端的 JavaScript 错误异常处理提供一些基础的指导。
先来简单介绍一下 JavaScript 中的 Error 对象,通常 Error 对象由重要的两部分组成,包含了 error.message 错误信息和 error.stack 错误追溯栈。
产生一个错误很简单,比如在 foo.js 中直接调用一个不存在的 callback 函数。
// foo.jsfunction 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 + 'nn' + err.stack;};
原始的错误数据中会展示出错误追溯栈中的 Source URL。
callback is not definedReferenceError: 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 事件来捕获全局的 JavaScript 执行错误,标准的浏览器在响应该事件时会依次提供 5 个参数。
window.onerror = function(message, source, lineno, colno, 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 错误,但还有不少场景在全局是捕获不到的。
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 函数中的错误无法被全局捕获。
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 和 Promise 一样,发生错误不会被全局的 window.onerror 捕获,所以在使用时如果有报错,需要手动增加 try/catch 语句。
匿名函数的使用在 JavaScript 中很常见,但是当出现匿名函数的报错时,在错误追溯栈中会以 anonymous 来标识错误,为了排查错误方便,可以将函数进行命名,或者使用函数的 displayName 属性。
函数如果有 displayName 属性,在错误栈中会展示该属性值,如果用于命名重要的业务逻辑属性,将有效帮助排查错误。
上面说了很多错误捕获的注意点,如果要主动的抛错,都会使用 throw 来抛错,常见的几种抛错方法如下:
throw new Error('Problem description.') // 方法 1throw Error('Problem description.') // 方法 2throw 'Problem description.' // 方法 3throw 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
通过对错误信息的二次封装,可以增加一些有利于快速定位错误的额外信息。
原作者:雨夜带刀's Blog