美文网首页程序员
一知半解的 Error in Javascript

一知半解的 Error in Javascript

作者: kdepp | 来源:发表于2016-08-18 11:37 被阅读1510次

Error 常识

在 javascript 中,关于 Error,我们最熟悉的莫过于两类:

  • 捕获异常 try { } catch (error) { }
  • 抛出异常 throw new Error()

无法 try catch 的 Error

我之前对 Error 的印象也就到这里为止,直到出现了下面两个 case:

  • 在异步操作中的异常
    try {
      setTimeout(function () {
        throw new Error();
      }, 0);
    } catch (e) {
      // won't be caught
      console.log(e);
    }
    
  • 在 promise then 里的异常
    try {
      Promise.resolve().then(function () {
         throw new Error();
      });
    } catch (e) {
      // won't be caught
      console.log(e);
    }
    

以上两个 throw error,都无法通过在外围 try catch 来捕获,其原因分别为:

  • try catch 只能捕获在其中执行的同步代码所抛出的异常
  • Promise 对 then 里回调函数都进行了异常捕获,只会继续向下一个 then 的第二参数 onReject 传递,而不会对外抛出。(也有一些 Promise 库会提供抛出 Uncaught Error 的功能)

不过 Promise 对异常的捕获,也仅限于同步代码的异常,所以 Promise 内向外抛出异常也可以使用简单的 setTimeout。但是要注意,外围的 try catch 依然无法捕获该错误。

try {
  Promise.resolve().then(function () {
    setTimeout(function () {
      throw new Error();
    });
  });
} catch (e) {
  // won't be caught
  console.log(e)
}

如何捕获 Uncaught Error

那些没有在同步代码内 try catch 的异常,会产生如下效果

  • 后续同步代码停止运行
  • 在 NodeJS 下,直接退出运行 (可避免)
  • 在浏览器下,之前设定的异步操作依然可以触发执行(包括timeout, dom 事件)

要捕获 Uncaught Error ,我们可以这么做

// 在 NodeJS 下,可以避免程序直接退出
process.on('uncaughtException', function (err) {
  ...
});

// 在浏览器中
window.onerror = function(msg, file, line, col, error) {
  ...
};

Stack Trace

每当异常发生,最重要的事情就是找到问题源头,这也是 Error 对象存在的价值。为什么这样说呢?有下面2点原因:

  1. throw 并不一定要接一个 Error,我们可以 throw 任何东西。
  2. 一个 Error 对象,不仅有 Error Message,更重要的是有函数调用栈 err.stack

// stack trace example
TypeError: lts.enableLongStackTrace is not a function
at Object.<anonymous> (/Users/kdepp/projects/me/lst/test/spec.js:3:5)
at Module._compile (module.js:435:26)
at Object.Module._extensions..js (module.js:442:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:311:12)
at Module.require (module.js:366:17)
at require (module.js:385:17)


需要注意的是,每个浏览器引擎对 Error.prototype.stack 的实现并不完全相同,且 stack 也还并没有一个 ecma 的标准。

本文后续,将以 V8 对 Error.prototype.stack 的实现来作为讨论依据。

### stack 格式
- 第一行,Error.toString()
- 后续多行,stack frame 信息,包含:
- 函数名
- 文件名
- 行数
- 列数 

### stack 条目数量
可以通过设置 Error.stackTraceLimit 来控制 stack frame 的显示条数。
- 正数:即实际会显示的上限
- 负数:不显示任何 stack frame
- Infinity:显示所有 stack frame

### Error 上和 stack 有关的函数
关于 V8 的 stack,有三点需要注意:
- stack 的内容可以做定制化,生成实际 stack 文本的入口是 Error.prepareStackTrace,我们可以重写这个函数
- stack 的内容只有在被使用时,才会去调用 prepareStackTrace 渲染其内容
- stack 这个属性我们还可以武装到其他任意对象上

#### Error.prepareStackTrace(error, structuredStackTrace)
- **param**: error
- Error 对象
- **param**: structuredStackTrace
- 一个数组的 CallSite 对象,包含错误的函数名、行数等信息

// 简化的 prepareStackTrace 实现
Error.prepareStackTrace = function (error, structuredStackTrace) {
var trace = structuredStackTrace.map(function (callSite) {
return ' at: ' + callSite.getFunctionName() + ' ('
+ callSite.getFileName() + ':'
+ callSite.getLineNumber() + ':'
+ callSite.getColumnNumber() + ')';
});
return error.toString() + "\n" + trace.join("\n")
};


#### CallSite 对象 API
包含  getThis, getTypeName, getFunction, getFunctionName, getMethodName, getFileName, getLineNumber, getColumnNumber, getEvalOrigin, isTopLevel, isEval, isNative, isConstructor,具体含义可参考[V8 wiki](https://github.com/v8/v8/wiki/Stack%20Trace%20API#customizing-stack-traces)

#### Error.captureStackTrace(error, constructorOpt)
- **param**: error
  - 希望被装上 stack 属性的任意对象
- **param**: constructorOpt
  - 原 stack 中,从 constructorOpt 往上的 stack frame 都会被忽略,此参数可以省略

captureStackTrace 最大的作用就是让我们可以 throw 自己定制的 Error 类型,又不失 stack trace 信息。

function MyError(msg) {
this.msg = msg;
Error.captureStackTrace(this, MyError);
}

MyError.prototype.toString = function () {
return 'Oops, MyError: ' + this.msg;
};

throw new MyError('msg');


## Long Stack Trace
stack trace 也有短板,问题同样出在异步操作。正常的 stack trace 遇到异步回调就会丢失绑定回调前的 stack frame,来看个例子:

var foo = function () {
throw new Error('msg');
};

var bar = function () {
setTimeout(foo);
};

bar();
/*
Error: msg
at foo [as _onTimeout] (repl:2:7)
at Timer.listOnTimeout (timers.js:92:15)
*/

foo();
/*
Error: msg
at foo (repl:2:7)
at repl:1:1
at REPLServer.defaultEval (repl.js:164:27)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.<anonymous> (repl.js:393:12)
at emitOne (events.js:82:20)
at REPLServer.emit (events.js:169:7)
at REPLServer.Interface._onLine (readline.js:210:10)
at REPLServer.Interface._line (readline.js:549:8)
*/


在实际开发过程中,异步回调的例子数不胜数,尤其是在 NodeJS 环境下更是如此,如果不能知道异步回调之前的触发位置,会给 debug 带来很大的难度。这时,就出现了一个概念叫 long Stack Trace。

long Stack Trace 并不是 Javascript 原生就支持的东西,所以要拥有这样的 debug 功能,就需要我们做一些 hack,幸好在 V8 环境下,所有 hack 所需的 API,V8 都已经提供了。

思路是唯一的, 就是要在异步回调里,记录之前的 stack trace

### 针对异步回调
对于异步回调,需要做的就是在所有会产生异步操作的 API,都做一些手脚,这些 API 包括
* setTimeout, setInterval, setImmediate
* nextTick, nextDomainTick
* EventEmitter.addEventListener
* EventEmitter.on
* Ajax XHR

在这方面,做的比较的库可以参考:
* https://github.com/mattinsler/longjohn
* https://github.com/tlrobinson/long-stack-traces

### 针对 Promise

很多 Promise 库都包含了 longStackTrace 的功能,比如 bluebird。不过原生的 Promise 还不支持这个功能

### 打包解决方案
https://github.com/angular/zone.js
尚未细看 zone.js 的内容,不过自从 node 的 domain 模块被设为 deprecated, zone.js 好像就是异步异常捕获的最好选择,后面有时间,准备再细看一下 zone.js。

相关文章

网友评论

    本文标题:一知半解的 Error in Javascript

    本文链接:https://www.haomeiwen.com/subject/mnxvsttx.html