前言
人无完人,所以代码总会出异常的,异常并不可怕,关键是怎么处理
什么是异常
程序发生了意想不到的情况,影响到了程序的正确运行
从根本上来说,异常就是一个普通的对象,其保存了异常发生的相关信息,比如错误码、错误信息等。以 JS 中的标准内置对象 Error 为例,其标准属性有 message。许多宿主环境额外增加了 filename 和 stack 等属性
错误只有被 throw,才会产生异常,不被抛出的错误不会产生异常。比如直接new Error()
甚至打印 Error 但是不 throw,也是不会产生异常
异常的分类
编译时异常
源代码在编译成可执行代码之前产生的异常,无需执行即有异常。编译、语法解析发生错误。编译型语言对于这种很常见的,但是解析型的 js 也是会有编译型异常。通常是非合法的 js 语句、ts 编译报错
console.log(1)
let 1 // Uncaught SyntaxError: Unexpected number
function test() {
console.log(1)
await 1
}
代码本身不会执行就抛异常,不会处理到打印 1 的阶段。这种情况通常不会有实际影响,因为 babel/ts 等工具处理时就会直接报错。除非不经编译直接写代码,例如有时候我们直接写在 html 中写的一些代码
运行时异常
代码被执行之后产生的异常。这些通常是很难提前发现的,因为代码实际运行中会遇到。比较常见的如TypeError: Cannot read properties of undefined
这样的读取了undefined
的属性。运行时异常对比编译时异常的特点是代码执行到异常代码前都是会正常执行的
执行到a.b.c
前的打印能成功,异常抛出后后面的语句就不能执行了。运行时异常即可是这种引擎层面抛出的也可以是代码手动抛出的
而上面说的编译时异常,即使异常语句前的正常语句也是不会执行
异常传播
异常抛出就像事件冒泡一样具有传递性。如果一个异常没有被 catch,它会沿着函数调用栈一层层传播直到栈空。
异常会不断传播直到遇到第一个 catch。 如果都没有捕获,会抛出类似 unCaughtError,表示发生了一个异常,未被捕获的异常通常会被打印在控制台上
error 对象
Error
本身作为函数直接调用和被 new 调用的效果是一样的
const a = Error('a')
const b = new Error('b')
javascript 规范中总共有 8 中错误类型构造函数
- Error – 错误对象
- SyntaxError --解析过程语法错误(上面提到的编译时异常)
- TypeError – 不属于有效类型(上面举例的运行时异常)
- ReferenceError – 无效引用(严格模式下直接访问一个未定义的变量)
- RangeError – 数值超出有效范围
- URIError – 解析 URI 编码出错
- EvalError – 调用 eval 函数错误
- InternalError – Javascript 引擎内部错误的异常抛出, “递归太多”
Error 是错误的基类,其他类型都继承 Error 这个类
console.log(Object.getPrototypeOf(SyntaxError) === Error); // true
console.log(Object.getPrototypeOf(TypeError) === Error); // true
console.log(Object.getPrototypeOf(ReferenceError) === Error); // true
console.log(Object.getPrototypeOf(RangeError) === Error); // true
console.log(Object.getPrototypeOf(URIError) === Error); // true
console.log(Object.getPrototypeOf(EvalError) === Error); // true
默认的 error 对象只有一个 message 信息,很多时候对于错误的细分是很不好使,一般可以通过扩展这个错误对象,抛异常时抛出自定义的错误对象,在异常处理或时实现更精细化的处理
class ApiError extends Error {
constructor(message, code) {
super(message);
this.code = code
}
}
const err = new ApiError('xxx', 404)
err instanceof ApiError
一种常见的应用就是在 axios 处理的异常中抛出一个扩展的 ApiError 对象,传递错误信息、错误等,在错误处理时对于这种错误进行特殊处理。如自定义上报、catch 住不作为 js 异常上报。不进行这种处理的话平时比较常见的情况就是会造成 slardar 的中 js 错误部分会有很多 axios 抛出的噪音
除了扩展错误对象,目前有一个处于 stage 4 的 Error Cause 提案 https://github.com/tc39/proposal-error-cause。这个提案也是由阿里推进的国内的首个es提案
Chrome 96 版本目前还不可用,firefox 可用
通过传递给 Error 构造函数的第二个参数一个 cause 属性为一个 Error 对象,即可看到是哪个错误具体产生当前的错误,对于一些调用链路比较深的可可能存在多个异常抛出情况这个特性还是相当好用的,可以准确追踪。Error Cause 当然用自定义扩展错误也能够实现这个功能
async function doJob() {
const rawResource = await fetch('//domain/resource-a')
.catch(err => {
throw new Error('Download raw resource failed', {
cause: err });
});
const jobResult = doComputationalHeavyJob(rawResource);
await fetch('//domain/upload', {
method: 'POST', body: jobResult })
.catch(err => {
throw new Error('Upload job result failed', {
cause: err });
});
}
try {
await doJob();
} catch (e) {
console.log(e);
console.log('Caused by', e.cause);
}
// Error: Upload job result failed
// Caused by TypeError: Failed to fetch
Error 的相关 api
- 改变堆栈帧数
默认情况下,V8 引发的几乎所有错误都具有一个 stack 属性,该属性保存最顶层的 10 个堆栈帧,格式为字符串 at xxx
Error.stackTraceLimit
Error.stackTraceLimit
属性指定堆栈跟踪收集的堆栈帧数。默认值为 10
,可以设置为任何有效的 JavaScript 数值。 更改将影响值更改后捕获的任何堆栈跟踪。如果设置为非数字值,或设置为负数,则堆栈跟踪将不会捕获任何帧
- 收集自定义异常
Error.captureStackTrace(error, constructorOpt)
这个 API 可以给自定义对象追加 stack 属性,达到模拟 Error 的效果,追加的 stack 表示调用 Error.captureStackTrace()
的代码中的位置的字符串。
function CustomError(message) {
this.message = message;
this.name = CustomError.name;
Error.captureStackTrace(this); // 给对象追加stack属性
}
try {
throw new CustomError('msg');
} catch (e) {
console.log(e)
}
需要注意的是stack
属性对于不同浏览器的格式是不一致的,通常而言监控 sdk 会统一做处理
这个方法支持传递一个constructorOpt
参数,表示所有 constructorOpt
以上的帧,包括 constructorOpt
,都将从生成的堆栈跟踪中省略。具体的差异如下
使用这个参数可以用于调用栈过深时隐藏深层次的一些调用细节
- sourcemap 还原错误
还原错误也是利用了 error 对象的 stack 属性。可以使用stacktracey
和source-map
实现根据错误堆栈还原到实际发生错误的代码
线上代码经过压缩后一般只有 1 行,对于定位原始错误是很困难的。并且默认的e.stack
属性是个字符串,可以借助stacktracey
进行解析并结合source-map
进行反解
const sourceMap = require('source-map');
const SourceMapConsumer = sourceMap.SourceMapConsumer;
const Stacktracey = require('stacktracey');
const errorStack = '...'; // 错误信息
const sourceMapFileContent = '...'; // sourcemap文件内容
const tracey = new Stacktracey(errorStack); // 解析错误信息
const sourceMapContent = JSON.parse(sourceMapFileContent);
const consumer = await new SourceMapConsumer(sourceMapContent);
for (const frame of t