前言

随着前端监控体系的日益完善,前端工程师们也对异常更加地关注,大家也更重视异常捕获与消息上报,但是产生的原因以及处理办法所谈甚少,那么前端异常到底有哪些呢?

什么是异常

异常:Exception,即预料之外的事情,在程序执行的时候会打断程序正常运行。

有哪些

ECMA-262 白皮书 13 版中描述了 8 种异常:

  • SyntaxError:语法异常
  • ReferenceError:引用异常
  • RangeError:范围异常
  • Error:异常基类
  • InternalError:内部异常
  • TypeError: 类型异常
  • EvalError: Eval 方法异常
  • URIError: URI 相关方法产生的异常

SyntaxError

在引擎执行代码之前,编译器需要对 js 进行编译,编辑阶段包括:词法分析,语法分析。
编译阶段发生的异常都是 SyntaxError,但 SyntaxError 不完全都发生于编译阶段;

常见的 SyntaxError:

  • SyntaxError: Invalid or unexpected token
  • SyntaxError:Unexpected token u in JSON at position 0
  • SyntaxError:Unexpected token ‘<’
  • SyntaxError:Unexpected identifier

ReferenceError

引用异常,比较常见,类似于 Java 语言中最著名的空指针异常 (Null Pointer Exception,NPE).

常见的有:

  • ReferenceError:$ is not defined
  • ReferenceError:Can't find variable: $

TypeError

TypeError 在对值进行不合理操作时会发生,比如试图对一个非函数类型的值进行函数调用,或者引用 null 或 undefined 类型的值中的属性,那么引擎会抛出这种类型的异常。

常见的有:

  • TypeError:Cannot read property 'length' of undefined

RangeError

范围错误,比如:

  • new Array(-20) 会导致 RangeError: Invalid array length
  • 递归等消耗内存的程序会导致 RangeError: Maximum call stack size exceeded

Error 与自定义异常

Error 是所有错误的基类,其他错误类型继承该类型。所有错误类型都共享相同的属性。

  • Error.prototype.message 错误消息。对于用户创建的 Error 对象,这是构造函数的第一个参数提供的字符串
  • Error.prototype.name 错误名称。这是由构造函数决定的
  • Error.prototype.stack 错误堆栈

Error: Script Error

它是 Error 类型中最常见的一种;由于没有具体异常堆栈和代码行列号,成为可最神秘的异常之一。
由于浏览器基于安全考虑效避免敏感信息无意中被第三方 (不受控制的) 脚本捕获到,浏览器只允许同域下的脚本捕获具体的错误信息。
但大部分的 JS 文件都存放在 CDN 上面,跟页面的域名不一致。做异常监控只能捕获 Error: Script Error. 无法捕获堆栈和准确的信息。

其他异常

InternalError

这种异常极为少见,在 JS 引擎内部发生,示例场景通常为某些成分过大,例如:

  • too many switch cases(过多 case 子句)
  • too many parentheses in regular expression(正则表达式中括号过多)
  • array initializer too large(数组初始化器过大)
EvalError

在 eval() 方法执行过程中抛出 EvalError 异常

URIError

用来表示以一种错误的方式使用全局 URI 处理函数而产生的错误.
decodeURI, decodeURIComponent, encodeURI, encodeURIComponent 这四个方法会产生这种异常;
比如执行 decodeURI('%%') 的异常:Uncaught URIError: URI malformed.

异常处理

finally

finally 在 try-catch 语句中是可选的,finally 子句一经使用,其代码无论如何都会执行。

throw

throw new Error('Boom');
什么时候应该手动抛出异常呢?
一个指导原则就是可预测程序在某种情况下不能正确进行下去,需要告诉调用者异常的详细信息,而不仅仅是异常内容本身。

assert

断言,如果表达式不符合预期,就抛出一个错误。
assert 方法接受两个参数,当第一个参数对应的布尔值为 true 时,不会有任何提示,返回 undefined。当第一个参数对应的布尔值为 false 时,会抛出一个错误,该错误的提示信息就是第二个参数设定的字符串。

异步中的异常

非同步的代码,在事件循环中执行的,就无法通过 try catch 到。
主要注意的是,Promise 的 catch 方法用于处理 rejected 状态,而非处理异常。Rejected 状态未处理的话会触发 Uncaught Rejection. 后者可以通过如下方式进行统一的监听。

window.onunhandledrejection = (event) => {
  console.warn(`REJECTION: ${event.reason}`);
};

tips: await 这种 Promise 的同步写法,通常会被开发者忽略 rejected 的处理,可以用 try catch 来捕获。

异常监控

服务端通常会通过服务器的日志进行异常监控,比如观察单台服务器的日志输出,或 kibana 可视化查询。
前端异常监控与之最大的不同,就是需要把客户端发生的异常数据通过网络再收集起来。

参考: