JavaScript Errors 指南

前端达人

共 1311字,需浏览 3分钟

 ·

2020-11-20 07:17

5f497c61313e19ff8e156c8541a36388.webp

英文:mknichel 译文:Jocs

https://github.com/Jocs/jocs.github.io/issues/1

在README文件中包含了这么多年我对JavaScript errors的学习和理解,包括把错误报告给服务器、在众多bug中根据错误信息追溯产生错误的原因,这些都使得处理JavaScript 错误变得困难。浏览器厂商在处理JavaScript错误方面也有所改进,但是保证应用程序能够稳健地处理JavaScript错误仍然有提升的空间。

Introduction


捕获、报告、以及修改错误是维护和保持应用程序健康稳定运行的重要方面。由于Javascript代码主要是在客户端运行、客户端环境又包括了各种各样的浏览器。因此使得消除应用程序中 JS 错误变得相对困难。关于如何报告在不同浏览器中引起的 JS 错误依然也没有一个正式的规范。除此之外,浏览器在报告JS错误也有些bug,这些原因导致了消除应用程序中的JS 错误变得更加困难。这篇文章将会以以上问题作为出发点,分析JS错误的产生、JS错误包含哪些部分、怎么去捕获一个JS错误。期待这篇文章能够帮助到以后的开发者更好的处理JS错误、不同浏览器厂商能够就JS错误找到一个标准的解决方案。

JavaScript 错误剖析


一个JavaScript 错误由 错误信息(error message) 和 追溯栈(stack trace) 两个主要部分组成。错误信息是一个字符串用来描述代码出了什么问题。追溯栈用来记录JS错误具体出现在代码中的位置。JS 错误可以通过两种方式产生、要么是浏览器自身在解析JavaScript代码时抛出错误,要么可以通过应用程序代码本身抛出错误。(译者注:例如可以通过throw new Error() 抛出错误)

产生一个JavaScript 错误

当JavaScript代码不能够被浏览器正确执行的时候,浏览器就会抛出一个JS错误,或者应用程序代码本身也可以直接抛出一个JS错误。

例如:

var a = 3;

a();


在如上例子中,a 变量类型是一个数值,不能够作为一个函数来调用执行。浏览器在解析上面代码时就会抛出如下错误TypeError: a is not a function 并通过追溯栈指出代码出错的位置。

开发者也通常在条件语句中当条件不满足的前提下,抛出一个错误,例如:

if (!checkPrecondition()) {
  throw new Error("Doesn't meet precondition!");
}


在这种情况下,浏览器控制台中的错误信息如是Error: Dosen’t meet precondition!. 这条错误也会包含一个追溯栈用来指示代码错误的位置,通过浏览器抛出的错误或是通过应用本身抛出的错误可以通过相同的处理手段来处理。

开发者可以通过不同方式来抛出一个JavaScript 错误:

  • throw new Error(‘Problem description.’)

  • throw Error(‘Problem description.’) <— equivalent to the first one

  • throw ‘Problem description.’ <— bad

  • throw null <— even worse


直接通过throw 操作符抛出一个字符串错误(**译者注:上面第三种方式)或者或者抛出null 这两种方式都是不推荐的,因为浏览器无法就以上两种方式生成追溯栈,也就导致了无法追溯错误在代码中的位置,因为推荐抛出一个Error 对象,Error对象不仅包含一个错误信息,同时也包含一个追溯栈这样你就可以很容易通过追溯栈找到代码出错的行数了。

Error Messages

不同浏览器在就错误信息的格式有不同的实现形式,比如上面的例子,在把一个原始类型的变量当做函数执行的时候,不同浏览器都在试图找到一个相同的方式来抛出这个错误,但是又没有统一标准,因此相同的形式也就没有了保证,比如在Chrome和Firefox中,会使用{0} is not a function 形式来抛出错误信息,而IE11 会抛出Function expected 错误信息(IE浏览器甚至不会指出是哪个变量被当做了函数调用而产生错误)

然而,不同浏览器在就错误信息上也有可能产生分歧,比如当switch 语句中有多个default 语句时,Chrome会抛出 “More than one default clause in switch statement” 而FireFox会抛出”more than one switch default”. 当新特性加入到JavaScript语言中时,错误信息也应该实时更新。当处理容易产生混淆代码导致的错误时,往往也需要使用到不同的处理手段。

你可以通过如下地址找到不同浏览器厂商在处理错误信息上面的做法:

  • Firefox - http://mxr.mozilla.org/mozilla1.9.1/source/js/src/js.msg

  • Chrome - https://code.google.com/p/v8/source/browse/branches/bleeding_edge/src/messages.js

  • Internet Explorer - https://github.com/Microsoft/ChakraCore/blob/4e4d4f00f11b2ded23d1885e85fc26fcc96555da/lib/Parser/rterrors.h


Browsers will produce different error messages for some exceptions.

追溯栈格式

追溯栈是用来描述错误出现在代码中什么位置。追溯栈通过一系列相互关联的帧组成,每一帧描述一行特定的代码,追溯栈最上面的那一帧就是错误抛出的位置,追溯栈下面的帧就是一个函数调用栈 - 也就是浏览器在执行JavaScript代码时一步一步怎么到抛出错误代码那一行的。

一个基本的追溯栈如下:

  at throwError (http://mknichel.github.io/javascript-errors/throw-error-basic.html:8:9)
  at http://mknichel.github.io/javascript-errors/throw-error-basic.html:12:3


追溯栈中的每一帧由以下三个部分组成:一个函数名(发生错误的代码不是在全局作用域中执行),发生错误的脚本在网络中的地址,以及发生错误代码的行数和列数。

遗憾的是,追溯栈还没有一个标准形式,因此不同浏览器厂商在实现上也是有差异的。

IE 11的追溯栈和Chrome 的追溯栈很相似,除了在全局作用域中的代码上有些差异:

  at throwError (http://mknichel.github.io/javascript-errors/throw-error-basic.html:8:3)
  at Global code (http://mknichel.github.io/javascript-errors/throw-error-basic.html:12:3)


Firefox 的追溯栈如下格式:

  throwError@http://mknichel.github.io/javascript-errors/throw-error-basic.html:8:9
  @http://mknichel.github.io/javascript-errors/throw-error-basic.html:12:3


Safari 的追溯栈格式和Firefox很相似,但是仍然有些出入:

  throwError@http://mknichel.github.io/javascript-errors/throw-error-basic.html:8:18
  global code@http://mknichel.github.io/javascript-errors/throw-error-basic.html:12:13


所有的浏览器厂商追溯栈基本信息差不多,但是格式上有些差异:

在上面Safari追溯栈的例子中,除了在追溯栈格式上和Chrome有差异外,发生错误的列数也和Chrome和Firefox不同。在不同的错误情境中,行数也会有所不同,比如如下代码:

(function namedFunction() { throwError(); })();

Chrome 会从throwError()开始计数行数,而IE11会从上面代码开始位置计算行数。这些不同浏览器之间在追溯栈格式上和计数上的差异也为后期解析追溯栈带来了困难。

通过如下网站 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack 了解更多关于追溯栈的问题。

不同浏览器厂商在追溯栈格式以及列数上都有可能存在差异

深入研究,浏览器厂商关于追溯栈还有很多细微差异,将在下面部分详细讨论。

为匿名函数取名

默认情况下,匿名函数没有名字,同时在追溯栈中要么表现为空字符串要么就是Anonymous function(根据不同浏览器会有区别)。为了提升代码的可调试性,你应该为所用的函数添加一个函数名,以使得其在追溯栈中出现,而不是空字符串或者Anonymous function。最简单的方法就是在所有的匿名函数前面加一个函数名,甚至该函数名不会在其他任何场合使用到。如下:

setTimeout(function nameOfTheAnonymousFunction() { ... }, 0);

上面代码的改变将使得追溯栈中也发生如下改变,从

at http://mknichel.github.io/javascript-errors/javascript-errors.js:125:17


变成了如下形式

at nameOfTheAnonymousFunction (http://mknichel.github.io/javascript-errors/javascript-errors.js:121:31)

上面给匿名函数添加姓名的方法可以保证函数名出现在追溯栈中,这样也使得代码更易调试,通过如下网站你可以了解更多关于代码调试的信息。http://www.html5rocks.com/en/tutorials/developertools/async-call-stack/

将函数赋值给一个变量

浏览器通常也会使用匿名函数赋值给的变量作为函数名,在追溯帧中出现。举个例子:

var fnVariableName = function() { ... };


浏览器会使用fnVariableName作为函数名在追溯栈中出现。

    at throwError (http://mknichel.github.io/javascript-errors/javascript-errors.js:27:9)
    at fnVariableName (http://mknichel.github.io/javascript-errors/javascript-errors.js:169:37)

浏览器厂商在追溯栈上甚至还有更加细微的差异,如果一个函数被赋值给了一个变量,并且这个函数定义在另外一个函数内,几乎所有的浏览器都会使用被赋值的变量作为追溯帧中的函数名,但是,Firefox有所不同,在Firefox中,会使用外面的函数名加上内部的函数名(变量名)作为追溯帧中的函数名。举个例子:

function throwErrorFromInnerFunctionAssignedToVariable() {
  var fnVariableName = function() throw new Error("foo"); };
  fnVariableName();
}

在Firefox中追溯帧格式如下:

throwErrorFromInnerFunctionAssignedToVariable/fnVariableName@http://mknichel.github.io/javascript-errors/javascript-errors.js:169:37


在其他的浏览器,追溯帧格式如下:

at fnVariableName (http://mknichel.github.io/javascript-errors/javascript-errors.js:169:37)

在一个函数定义在另外一个函数内部的情景下(闭包)Firefox会使用不同于其他浏览器厂商的格式来处理函数名

displayName 属性

除了IE11,函数名的展现也可以通过给函数定义一个displayName 属性,displayName会出现在浏览器的devtools debugger中。而Safari displayName还会出现在追溯帧中。

var someFunction = function() {};
someFunction.displayName = " # A longer description of the function.";

虽然关于displayName还没有官方的标准,但是该属性已经在主要的浏览器中实现了。通过如下网站你可以了解更多关于displayName的信息:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/displayName 和 http://www.alertdebugging.com/2009/04/29/building-a-better-javascript-profiler-with-webkit/

IE11不支持displayName属性

Safari displayName property bug Safari 会使用displayName作为函数名在追溯帧中出现

通过编程来获取追溯栈

当抛出一个错误但又没有追溯栈的时候(通过下面的内容了解更多),我们可以通过一些编程的手段来捕获追溯栈。

在Chrome中,可以简单的调用Error.captureStackTrace API来获取到追溯栈,关于该API的使用可以通过如下链接了解:https://github.com/v8/v8/wiki/Stack%20Trace%20API

举个例子:

function ignoreThisFunctionInStackTrace() {
  var err = new Error();
  Error.captureStackTrace(err, ignoreThisFunctionInStackTrace);
  return err.stack;
}

在其它浏览器中,追溯栈也可以通过生成一个错误,然后通过stack属性来获取追溯栈。

var err = new Error('');
return err.stack;

但是在IE10中,只有当错误真正抛出后才能够获取到追溯栈。

try {
  throw new Error('');
catch (e) {
  return e.stack;
}

如果上面的方法都起作用时,我们可以通过arguments.callee.caller 对象来粗糙的获取一个没有行数和列数的追溯栈,但是这种方法在ES5严格模式下不起作用,因此这种方法也不是一种推荐的做法。

Async stack traces

异步追溯栈

在JavaScript代码中异步代码是非常常见的。比如setTimeout的使用,或者Promise对象的使用,这些异步调用入口往往会给追溯栈带来问题,因为异步代码会生成一个新的执行上下文,而追溯栈又会重新形成追溯帧。

Chrome DevTools 已经支持了异步追溯栈,换句话说,追溯栈在追溯一个错误的时候也会显示引入异步调用的那一调用帧。在使用setTimeout的情况下,在Chrome中会捕获谁调用了产生错误的setTimeout 函数。关于上面内容,可以从如下网站获取信息:http://www.html5rocks.com/en/tutorials/developertools/async-call-stack/

一个异步追溯栈会采用如下形式:

  throwError    @   throw-error.js:2
  setTimeout (async)        
  throwErrorAsync   @   throw-error.js:10
  (anonymous function)  @   throw-error-basic.html:14

目前,异步追溯栈只有Chrome DevTools支持,而且只有在DevTools代开的情况下才会捕获,在代码中通过Error对象不会获取到异步追溯栈。

虽然可以模拟异步调用栈,但是这往往会代指应用性能的消耗,因为这种方法也显得并不可取。

Only Chrome supports async stack traces 只有Chrome DevTools原生支持异步追溯栈

命名行内JS代码或者使用eval情况

在追溯使用eval或者HTML 中写JS的情况,追溯栈通常会使用HTML的URL 以及代码执行的行数和列数。

例如:

  at throwError (http://mknichel.github.io/javascript-errors/throw-error-basic.html:8:9)
  at http://mknichel.github.io/javascript-errors/throw-error-basic.html:12:3

出于一些性能或代码优化的原因,HTML中往往会有行内脚本,而且这种情况下,URL, 行数、列数也有可能出错,为了解决这些问题,Chrome和Firefox 支持//# sourceURL= 声明,(Safari 和 IE 暂不支持)。通过这种形式声明的URL会在追溯栈中使用到,而且行数和列数也会通过