合理地处理堆栈信息能使你清除无用的数据, 而只专注于有用的数据. 同时, 当更好地理解 Errors 对象及其相关属性之后, 能有助于你更充分地利用 Errors.

(函数的)调用栈是怎么工作的

在谈论错误之前, 先要了解下(函数的)调用栈的原理:

当有一个函数被调用的时候, 它就被压入到堆栈的顶部, 该函数运行完成之后, 又会从堆栈的顶部被移除.

堆栈的数据结构就是后进先出, 以 LIFO (last in, first out) 著称.

例如:

function c() {

console.log('c');

}

function b() {

console.log('b');

c();

}

function a() {

console.log('a');

b();

}

a();

在上述的示例中, 当函数 a 运行时, 其会被添加到堆栈的顶部. 然后, 当函数 b 在函数 a 的内部被调用时, 函数 b 会被压入到堆栈的顶部. 当函数 c 在函数 b 的内部被调用时也会被压入到堆栈的顶部.

当函数 c 运行时, 堆栈中就包含了 a, b 和 c(按此顺序).

当函数 c 运行完毕之后, 就会从堆栈的顶部被移除, 然后函数调用的控制流就回到函数 b. 函数 b 运行完之后, 也会从堆栈的顶部被移除, 然后函数调用的控制流就回到函数 a. 最后, 函数 a 运行完成之后也会从堆栈的顶部被移除.

为了更好地在demo中演示堆栈的行为, 可以使用 console.trace() 在控制台输出当前的堆栈数据. 同时, 你要以从上至下的顺序阅读输出的堆栈数据.

function c() {

console.log('c');

console.trace();

}

function b() {

console.log('b');

c();

}

function a() {

console.log('a');

b();

}

a();

在 Node 的 REPL 模式中运行上述代码会得到如下输出:

Trace

at c (repl:3:9)

at b (repl:3:1)

at a (repl:3:1)

at repl:1:1 // <-- For now feel free to ignore anything below this point, these are Node's internals

at realRunInThisContextScript (vm.js:22:35)

at sigintHandlersWrap (vm.js:98:12)

at ContextifyScript.Script.runInThisContext (vm.js:24:12)

at REPLServer.defaultEval (repl.js:313:29)

at bound (domain.js:280:14)

at REPLServer.runBound [as eval] (domain.js:293:12)

正如所看到的, 当从函数 c 中输出时, 堆栈中包含了函数 a, b 以及c.

如果在函数 c 运行完成之后, 在函数 b 中输出当前的堆栈数据, 就会看到函数 c 已经从堆栈的顶部被移除, 此时堆栈中仅包括函数 a 和 b.

function c() {

console.log('c');

}

function b() {

console.log('b');

c();

console.trace();

}

function a() {

console.log('a');

b();

}

正如所看到的, 函数 c 运行完成之后, 已经从堆栈的顶部被移除.

Trace

at b (repl:4:9)

at a (repl:3:1)

at repl:1:1  // <-- For now feel free to ignore anything below this point, these are Node's internals

at realRunInThisContextScript (vm.js:22:35)

at sigintHandlersWrap (vm.js:98:12)

at ContextifyScript.Script.runInThisContext (vm.js:24:12)

at REPLServer.defaultEval (repl.js:313:29)

at bound (domain.js:280:14)

at REPLServer.runBound [as eval] (domain.js:293:12)

at REPLServer.onLine (repl.js:513:10)

Error对象和错误处理

当程序运行出现错误时, 通常会抛出一个 Error 对象. Error 对象可以作为用户自定义错误对象继承的原型.

Error.prototype 对象包含如下属性:

  • constructor–指向实例的构造函数

  • message–错误信息

  • name–错误的名字(类型)

上述是 Error.prototype 的标准属性, 此外, 不同的运行环境都有其特定的属性. 在例如 Node, Firefox, Chrome, Edge, IE 10+, Opera 以及 Safari 6+ 这样的环境中, Error 对象具备 stack 属性, 该属性包含了错误的堆栈轨迹. 一个错误实例的堆栈轨迹包含了自构造函数之后的所有堆栈结构.

如果想了解更多关于 Error 对象的特定属性, 可以阅读 MDN 上的这篇文章.

为了抛出一个错误, 必须使用 throw 关键字. 为了 catch 一个抛出的错误, 必须使用 try...catch 包含可能跑出错误的代码. Catch的参数是被跑出的错误实例.

如 Java 一样, JavaScript 也允许在 try/catch 之后使用 finally 关键字. 在处理完错误之后, 可以在finally 语句块作一些清除工作.

在语法上, 你可以使用 try 语句块而其后不必跟着 catch 语句块, 但必须跟着 finally 语句块. 这意味着有三种不同的 try 语句形式:

  • try...catch

  • try...finally

  • try...catch...finally

Try语句内还可以在嵌入 try 语句:

try {

try {

throw new Error('Nested error.'); // The error thrown here will be caught by its own `catch` clause

} catch (nestedErr) {

console.log('Nested catch'); // This runs

}

} catch (err) {

console.log('This will not run.');

}

也可以在 catch 或 finally 中嵌入 try 语句:

try {

console.log('The try block is running...');

} finally {

try {

throw new Error('Error inside finally.');

} catch (err) {

console.log('Caught an error inside the finally block.');

}

}

需要重点说明一下的是在抛出错误时, 可以只抛出一个简单值而不是 Error 对象. 尽管这看起来看酷并且是允许的, 但这并不是一个推荐的做法, 尤其是对于一些需要处理他人代码的库和框架的开发者, 因为没有标准可以参考, 也无法得知会从用户那里得到什么. 你不能信任用户会抛出 Error 对象, 因为他们可能不会这么做, 而是简单的抛出一个字符串或者数值. 这也意味着很难去处理堆栈信息和其它元信息.

例如:

function runWithoutThrowing(func) {

try {

func();

} catch (e) {

console.log('There was an error, but I will not throw it.');

console.log('The error\'s message was: ' + e.message)

}

}

function funcThatThrowsError() {

throw new TypeError('I am a TypeError.');

}

runWithoutThrowing(funcThatThrowsError);

如果用户传递给函数 runWithoutThrowing 的参数抛出了一个错误对象, 上面的代码能正常捕获错误. 然后, 如果是抛出一个字符串, 就会碰到一些问题了:

function runWithoutThrowing(func) {

try {

func();

} catch (e) {

console.log('There was an error, but I will not throw it.');

console.log('The error\'s message was: ' + e.message)

}

}

function funcThatThrowsString() {

throw 'I am a String.';

}

runWithoutThrowing(funcThatThrowsString);

现在第二个 console.log 会输出undefined. 这看起来不是很重要, 但如果你需要确保 Error 对象有一个特定的属性或者用另一种方式来处理 Error 对象的特定属性(例如 Chai的throws断言的做法), 你就得做大量的工作来确保程序的正确运行.

同时, 如果抛出的不是 Error 对象, 也就获取不到 stack 属性.

Errors 也可以被作为其它对象, 你也不必抛出它们, 这也是为什么大多数回调函数把 Errors 作为第一个参数的原因. 例如:

const fs = require('fs');

fs.readdir('/example/i-do-not-exist', function callback(err, dirs) {

if (err instanceof Error) {

// `readdir` will throw an error because that directory does not exist

// We will now be able to use the error object passed by it in our callback function

console.log('Error Message: ' + err.message);

console.log('See? We can use Errors without using try statements.');

} else {

console.log(dirs);

}

});

最后, Error 对象也可以用于 rejected promise, 这使得很容易处理 rejected promise:

new Promise(function(resolve, reject) {

reject(new Error('The promise was rejected.'));

}).then(function() {

console.log('I am an error.');

}).catch(function(err) {

if (err instanceof Error) {

console.log('The promise was rejected with an error.');

console.log('Error Message: ' + err.message);

}

});

处理堆栈

这一节是针对支持 Error.captureStackTrace的运行环境, 例如Nodejs.

Error.captureStackTrace 的第一个参数是 object, 第二个可选参数是一个 function.Error.captureStackTrace 会捕获堆栈信息, 并在第一个参数中创建 stack 属性来存储捕获到的堆栈信息. 如果提供了第二个参数, 该函数将作为堆栈调用的终点. 因此, 捕获到的堆栈信息将只显示该函数调用之前的信息.

用下面的两个demo来解释一下. 第一个, 仅将捕获到的堆栈信息存于一个普通的对象之中:

const myObj = {};

function c() {

}

function b() {

// Here we will store the current stack trace into myObj

Error.captureStackTrace(myObj);

c();

}

function a() {

b();

}

// First we will call these functions

a();

// Now let's see what is the stack trace stored into myObj.stack

console.log(myObj.stack);

// This will print the following stack to the console:

//    at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack

//    at a (repl:2:1)

//    at repl:1:1 <-- Node internals below this line

//    at realRunInThisContextScript (vm.js:22:35)

//    at sigintHandlersWrap (vm.js:98:12)

//    at ContextifyScript.Script.runInThisContext (vm.js:24:12)

//    at REPLServer.defaultEval (repl.js:313:29)

//    at bound (domain.js:280:14)

//    at REPLServer.runBound [as eval] (domain.js:293:12)

//    at REPLServer.onLine (repl.js:513:10)

从上面的示例可以看出, 首先调用函数 a(被压入堆栈), 然后在 a 里面调用函数 b(被压入堆栈且在a之上), 然后在 b 中捕获到当前的堆栈信息, 并将其存储到 myObj 中. 所以, 在控制台输出的堆栈信息中仅包含了 a和 b 的调用信息.

现在, 我们给 Error.captureStackTrace 传递一个函数作为第二个参数, 看下输出信息:

const myObj = {};

function d() {

// Here we will store the current stack trace into myObj

// This time we will hide all the frames after `b` and `b` itself

Error.captureStackTrace(myObj, b);

}

function c() {

d();

}

function b() {

c();

}

function a() {

b();

}

// First we will call these functions

a();

// Now let's see what is the stack trace stored into myObj.stack

console.log(myObj.stack);

// This will print the following stack to the console:

//    at a (repl:2:1) <-- As you can see here we only get frames before `b` was called

//    at repl:1:1 <-- Node internals below this line

//    at realRunInThisContextScript (vm.js:22:35)

//    at sigintHandlersWrap (vm.js:98:12)

//    at ContextifyScript.Script.runInThisContext (vm.js:24:12)

//    at REPLServer.defaultEval (repl.js:313:29)

//    at bound (domain.js:280:14)

//    at REPLServer.runBound [as eval] (domain.js:293:12)

//    at REPLServer.onLine (repl.js:513:10)

//    at emitOne (events.js:101:20)

当将函数 b 作为第二个参数传给 Error.captureStackTraceFunction 时, 输出的堆栈就只包含了函数 b 调用之前的信息(尽管 Error.captureStackTraceFunction 是在函数 d 中调用的), 这也就是为什么只在控制台输出了 a. 这样处理方式的好处就是用来隐藏一些与用户无关的内部实现细节.

参考

JavaScript Errors and Stack Traces in Depth

摘抄至:

作者:dwqs

github.com/dwqs/blog/issues/49

javascript 错误处理和堆栈追踪浅析的更多相关文章

  1. 【转】JavaScript错误处理和堆栈追踪

    原文: https://www.cnblogs.com/caoru/p/6699583.html --------------------------------------------------- ...

  2. JavaScript错误处理和堆栈追踪

    转自:https://github.com/dwqs/blog/issues/49 有时我们会忽略错误处理和堆栈追踪的一些细节, 但是这些细节对于写与测试或错误处理相关的库来说是非常有用的. 例如这周 ...

  3. 利用Decorator和SourceMap优化JavaScript错误堆栈

    配合源码阅读体验更佳. 最近收到用户吐槽 @cloudbase/js-sdk(云开发Cloudbase的JavaScript SDK)的报错信息不够清晰,比如下面这条报错: 这属于业务型报错,对于熟悉 ...

  4. JavaScript错误处理

    JavaScript 错误 - Throw.Try 和 Catch JavaScript 测试和捕捉 try 语句允许我们定义在执行时进行错误测试的代码块. catch 语句允许我们定义当 try 代 ...

  5. 【转】Javascript错误处理——try…catch

    无论我们编程多么精通,脚本错误怎是难免.可能是我们的错误造成,或异常输入,错误的服务器端响应以及无数个其他原因. 通常,当发送错误时脚本会立刻停止,打印至控制台. 但try...catch语法结构可以 ...

  6. 网站发布IIS后堆栈追踪无法获取出错的行号

    一.问题起因 系统发布上线后,有时会发生错误,那么错误的记录就很重要,它对于错误的排查和问题的发现有着重要的作用,通常我们采取的方式为Log日志文件记录和数据库错误记录.文本不会讨论错误记录的方式以及 ...

  7. 10 种最常见的 Javascript 错误(频率最高)

    本文是小编给大家收藏的JavaScript 中频度最高的 10 种错误,我们会告诉你什么原因导致了这些错误,以及如何防止这些错误发生.写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学 ...

  8. JavaScript错误/异常处理

    JavaScript Try...Catch 语句 介绍:JavaScript中的try...carch语句的作用和C#中的try...catch语句的作用一样, 都是捕获并处理异常. 语法: try ...

  9. Visual Studio 2013中因SignalR的Browser Link引起的Javascript错误一则

    众所周知Visual Studio 2013中有一个由SignalR机制实现的Browser Link功能,意思是开发人员可以同时使用多个浏览器进行调试,当按下IDE中的Browser Link按钮后 ...

随机推荐

  1. svn 要求commit提交必须加注释(日志) hook

    #vim /data/svn/mysvn/hook/pre-commit #!/bin/bashREPOS="$1"TXN="$2" #RES="OK ...

  2. 实现Windows程序的数据的绑定

    1.创建DataSet对象 语法: DataSet  数据集对象  =new  DataSet("数据集的名称字符串"); 语法中的参数是数据集的名称字符串,可以有,也可以没有.如 ...

  3. Oracle修改字段类型方法小技巧

    有一个表名为tb,字段段名为name,数据类型nchar(20). 1.假设字段数据为空,则不管改为什么字段类型,可以直接执行:alter table tb modify (name nvarchar ...

  4. SSH相关知识

    SSH(Secure Shell, 安全Shell协议)是一种加密的网络传输协议,经常用于安全的远程登录. SSH只是一种协议,可以有多种实现. OPENSSH是一种应用广泛的实现. sshd是dae ...

  5. ssm中iReport报表使用json数据源过程体会

    前言:做这个一定要有耐心,因为报表本就是数据杂糅到规整的过程,这篇心得会细讲每一步操作,如果只想着一眼到位,建议close tab 在公司中遇到项目,大概是一个这样的需求,有一个列表和一个标题,需要把 ...

  6. 使用jmeter+ant进行接口自动化测试(数据驱动)之一:设计jmeter脚本

    最近在做接口测试,因为公司有使用jmeter做接口测试的相关培训资料,所以还是先选择使用jmeter来批量管理接口,进行自动化测试.话不多说,进入正题: 1.使用csv文件保存接口测试用例,方便后期对 ...

  7. 设置linux虚拟机的固定ip、防火墙的一些操作

    安装好虚拟机后,需要设置其固定ip,这样才可以连接该虚拟服务器 设置步骤如下 1.进入network-scripts目录 cd /etc/sysconfig/network-scripts 2.编辑n ...

  8. numpy.random.seed()方法

    先贴参考链接: https://stackoverflow.com/questions/21494489/what-does-numpy-random-seed0-do numpy.random.se ...

  9. 学号:201621123032 《Java程序设计》第12周学习总结

    1:本周学习总结 1.1:以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 2:面向系统综合设计-图书馆管理系统或购物车 2.1: 简述如何使用流与文件改造你的系统.文件中数据的格式如何? ...

  10. MySQL 自关联查询

    定义表areas,结构如下 id atitle pid 因为省没有所属的省份,所以可以填写为null 城市所属的省份pid,填写省所对应的编号id 这就是自关联,表中的某一列,关联了这个表中的另外一列 ...