一知半解的 Error in Javascript

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容