本文首发于公众号: 符合预期的CoyPan
写在前面
在前端项目中,由于JavaScript本身是一个弱类型语言,加上浏览器环境的复杂性,网络问题等等,很容易发生错误。做好网页错误监控,不断优化代码,提高代码健壮性是一项很重要的工作。本文将从Error开始,讲到如何捕获页面中的异常。文章较长,细节较多,请耐心观看。
前端开发中的Error
JavaScript中的Error
JavaScript中,Error
是一个构造函数,通过它创建一个错误对象。当运行时错误产生时,Error的实例对象会被抛出。构造一个Error的语法如下:
// message: 错误描述
// fileName: 可选。被创建的Error对象的fileName属性值。默认是调用Error构造器代码所在的文件的名字。
// lineNumber: 可选。被创建的Error对象的lineNumber属性值。默认是调用Error构造器代码所在的文件的行号。
new Error([message[, fileName[, lineNumber]]])
ECMAScript标准:
Error有两个标准属性:
-
Error.prototype.name
:错误的名字 -
Error.prototype.message
:错误的描述
例如,在chrome控制台中输入以下代码:
var a = new Error('错误测试');
console.log(a); // Error: 错误测试
// at :1:9
console.log(a.name); // Error
console.log(a.message); // 错误测试
Error只有一个标准方法:
-
Error.prototype.toString
:返回表示一个表示错误的字符串。
接上面的代码:
a.toString(); // "Error: 错误测试"
非标准的属性
各个浏览器厂商对于Error都有自己的实现。比如下面这些属性:
-
Error.prototype.fileName
:产生错误的文件名。 -
Error.prototype.lineNumber
:产生错误的行号。 -
Error.prototype.columnNumber
:产生错误的列号。 -
Error.prototype.stack
:堆栈信息。这个比较常用。
这些属性均不是标准属性,在生产环境中谨慎使用。不过现代浏览器差不多都支持了。
Error的种类
除了通用的Error构造函数外,JavaScript还有7个其他类型的错误构造函数。
- InternalError: 创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: "递归太多"。非ECMAScript标准。
- RangeError: 数值变量或参数超出其有效范围。例子:var a = new Array(-1);
- EvalError: 与eval()相关的错误。eval()本身没有正确执行。
- ReferenceError: 引用错误。 例子:console.log(b);
- SyntaxError: 语法错误。例子:var a = ;
- TypeError: 变量或参数不属于有效范围。例子:[1,2].split('.')
- URIError: 给 encodeURI或 decodeURl()传递的参数无效。例子:decodeURI('%2')
当JavaScript运行过程中出错时,会抛出上8种(上述7种加上通用错误类型)错误中的其中一种错误。错误类型可以通过error.name拿到。
你也可以基于Error构造自己的错误类型,这里就不展开了。
其他错误
上面介绍的都是JavaScript本身运行时会发生的错误。页面中还会有其他的异常,比如错误地操作了DOM。
DOMException
DOMException是W3C DOM核心对象,表示调用一个Web Api时发生的异常。什么是Web Api呢?最常见的就是DOM元素的一系列方法,其他还有XMLHttpRequest、Fetch等等等等,这里就不一一说明了。直接看下面一个操作DOM的例子:
var node = document.querySelector('#app');
var refnode = node.nextSibling;
var newnode = document.createElement('div');
node.insertBefore(newnode, refnode);
// 报错:Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
单从JS代码逻辑层面来看,没有问题。但是代码的操作不符合DOM的规则。
DOMException
构造函数的语法如下:
// message: 可选,错误描述。
// name: 可选,错误名称。常量,具体值可以在这里找到:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMException
new DOMException([message[, name]]);
DOMException
有以下三个属性:
-
DOMException.code
:错误编号。 -
DOMException.message
:错误描述。 -
DOMException.name
:错误名称。
以上面那段错误代码为例,其抛出的DOMException各属性的值为:
code: 8
message: "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."
name: "NotFoundError"
Promise
产生的异常
在Promise
中,如果Promise
被reject
了,就会抛出异常:PromiseRejectionEvent
。注意,下面两种情况都会导致Promise
被reject
:
- 业务代码本身调用了
Promise.reject
。 -
Promise
中的代码出错。
PromiseRejectionEvent
的构造函数目前在浏览器中大多都不兼容,这里就不说了。
PromiseRejectionEvent
的属性有两个:
-
PromiseRejectionEvent.promise
:被reject
的Promise
。 -
PromiseRejectionEvent.reason
:Promise
被reject
的原因。会传递给reject
。Promsie
的catch
中的参数。
加载资源出错
由于网络,安全等原因,网页加载资源失败,请求接口出错等,也是一种常见的错误。
关于错误的小结
一个网页在运行过程中,可能发生四种错误:
- JavaScript在运行过程,语言自身抛出的异常。
- JavaScript在运行过程中,调用Web Api时发生异常。
- Promise中的拒绝。
- 网页加载资源,调用接口时发生异常。
我认为,对于前两种错误,我们在平时的开发过程中,不用特别去区分,可以统一成:【代码出错】。
捕获错误
网页发生错误,开发者如何捕获这些错误呢 ? 常见的有以下方法。
try...catch...
try...catch…
大家都不陌生了。一般用来在具体的代码逻辑中捕获错误。
try {
throw new Error("oops");
}
catch (ex) {
console.log("error", ex.message); // error oops
}
当try-block
中的代码发生异常时,可以在catck-block
中将异常接住,浏览器便不会抛出错误。但是,这种方式并不能捕获异步代码中的错误,如:
try {
setTimeout(function(){
throw new Error('lala');
},0);
} catch(e) {
console.log('error', e.message);
}
这个时候,浏览器依然会抛出错误:Uncaught Error: lala
。
试想以下,如果我们将所有的代码合理的划分,然后都用try catch
包起来,是不是就可以捕获到所有的错误了呢?可以通过编译工具来实现这个功能。不过,try catch
是比较耗费性能的。
window.onerror
window.onerror = function(message, source, lineno, colno, error) { ... }
函数参数:
-
message
:错误信息(字符串) -
source
:发生错误的脚本URL(字符串) -
lineno
:发生错误的行号(数字) -
colno
:发生错误的列号(数字) -
error
:Error对象(对象)
注意,如果这个函数返回true
,那么将会阻止执行浏览器默认的错误处理函数。
window.addEventListener('error')
window.addEventListener('error', function(event) { ... })
我们调用Object.prototype.toString.call(event)
,返回的是[object ErrorEvent]
。可以看到event
是ErrorEvent
对象的实例。ErrorEvent
是事件对象在脚本发生错误时产生,从Event
继承而来。由于是事件,自然可以拿到target
属性。ErrorEvent
还包括了错误发生时的信息。
- ErrorEvent.prototype.message: 字符串,包含了所发生错误的描述信息。
- ErrorEvent.prototype.filename: 字符串,包含了发生错误的脚本文件的文件名。
- ErrorEvent.prototype.lineno: 数字,包含了错误发生时所在的行号。
- ErrorEvent.prototype.colno: 数字,包含了错误发生时所在的列号。
- ErrorEvent.prototype.error: 发生错误时所抛出的 Error 对象。
注意,这里的ErrorEvent.prototype.error
对应的Error对象,就是上文提到的Error
, InternalError
,RangeError
,EvalError
,ReferenceError
,SyntaxError
,TypeError
,URIError
,DOMException
中的一种。
window.addEventListener('unhandledrejection')
window.addEventListener('unhandledrejection', function (event) { ... });
在使用Promise
的时候,如果没有声明catch
代码块,Promise
的异常会被抛出。只能通过这个方法或者window.onunhandledrejection
才能捕获到该异常。
event
就是上文提到的PromiseRejectionEvent
。我们只需要关注其reason
就行。
window.onerror 和 window.addEventListener('error')的区别
- 首先是事件监听器和事件处理器的区别。监听器只能声明一次,后续的声明会覆盖之前的声明。而事件处理器则可以绑定多个回调函数。
- 资源( 或