JavaScript错误处理和堆栈追踪详解


Posted in Javascript onApril 18, 2017

有时我们会忽略错误处理和堆栈追踪的一些细节, 但是这些细节对于写与测试或错误处理相关的库来说是非常有用的. 例如这周, 对于 Chai 就有一个非常棒的PR, 该PR极大地改善了我们处理堆栈的方式, 当用户的断言失败的时候, 我们会给予更多的提示信息(帮助用户进行定位).

合理地处理堆栈信息能使你清除无用的数据, 而只专注于有用的数据. 同时, 当更好地理解 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 {
  throw new Error('First error');
} catch (err) {
  console.log('First catch running');
  try {
    throw new Error('Second error');
  } catch (nestedErr) {
    console.log('Second catch running.');
  }
}
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 相关文章推荐
Javascript 判断是否存在函数的方法
Jan 03 Javascript
JS截取url中问号后面参数的值信息
Apr 29 Javascript
JavaScript中的各种操作符使用总结
May 26 Javascript
jQuery使用Layer弹出层插件闪退问题
Dec 22 Javascript
jQuery仿IOS弹出框插件
Feb 18 Javascript
JS实现一个简单的日历
Feb 22 Javascript
微信小程序 出现错误:{&quot;baseresponse&quot;:{&quot;errcode&quot;:-80002,&quot;errmsg&quot;:&quot;&quot;}}解决办法
Feb 23 Javascript
详解基于Angular4+ server render(服务端渲染)开发教程
Aug 28 Javascript
jQuery实现验证表单密码一致性及正则表达式验证邮箱、手机号的方法
Dec 05 jQuery
Bootstrap的aria-label和aria-labelledby属性实例详解
Nov 02 Javascript
使用Node.js实现base64和png文件相互转换的方法
Mar 11 Javascript
Vue this.$router.push(参数)实现页面跳转操作
Sep 09 Javascript
微信小程序开发之从相册获取图片 使用相机拍照 本地图片上传
Apr 18 #Javascript
微信小程序实战之自定义抽屉菜单(7)
Apr 18 #Javascript
微信小程序--onShareAppMessage分享参数用处(页面分享)
Apr 18 #Javascript
微信小程序实战之自定义toast(6)
Apr 18 #Javascript
Jquery-data的三种用法
Apr 18 #jQuery
微信小程序实战之登录页面制作(5)
Mar 30 #Javascript
Angular2数据绑定详解
Apr 18 #Javascript
You might like
PHP 获取远程文件内容的函数代码
2010/03/24 PHP
PHP二维数组的去重问题解析
2011/07/17 PHP
php中cookie实现二级域名可访问操作的方法
2014/11/11 PHP
PHP面向对象程序设计之多态性的应用示例
2018/12/19 PHP
基于laravel-admin 后台 列表标签背景的使用方法
2019/10/03 PHP
PHP实现抽奖功能实例代码
2020/06/30 PHP
JS 常用校验函数
2009/03/26 Javascript
文本框的字数限制功能jquery插件
2009/11/24 Javascript
javascript eval和JSON之间的联系
2009/12/31 Javascript
js数据验证集合、js email验证、js url验证、js长度验证、js数字验证等简单封装
2010/05/15 Javascript
js取整数、取余数的方法
2014/05/11 Javascript
js实现动画特效的文字链接鼠标悬停提示的方法
2015/03/02 Javascript
javascript中setInterval的用法
2015/07/19 Javascript
jQuery+css3实现Ajax点击后动态删除功能的方法
2015/08/10 Javascript
js实现刷新页面后回到记录时滚动条的位置【两种方案可选】
2016/12/12 Javascript
IntelliJ IDEA 安装vue开发插件的方法
2017/11/21 Javascript
vue2.0路由切换后页面滚动位置不变BUG的解决方法
2018/03/14 Javascript
angularjs $http调用接口的方式详解
2018/08/13 Javascript
教你如何编写Vue.js的单元测试的方法
2018/10/17 Javascript
python执行系统命令后获取返回值的几种方式集合
2018/05/12 Python
python制作mysql数据迁移脚本
2019/01/01 Python
Python类的继承用法示例
2019/01/31 Python
NumPy 基本切片和索引的具体使用方法
2019/04/24 Python
Python csv模块使用方法代码实例
2019/08/29 Python
Python迭代器iterator生成器generator使用解析
2019/10/24 Python
Python利用matplotlib绘制约数个数统计图示例
2019/11/26 Python
Python异常继承关系和自定义异常实现代码实例
2020/02/20 Python
python实现俄罗斯方块小游戏
2020/04/24 Python
HTML5 embed标签定义和用法详解
2014/05/09 HTML / CSS
美国女性卫生用品公司:Thinx
2017/06/30 全球购物
Myprotein亚太地区:欧洲第一在线运动营养品牌
2020/12/20 全球购物
C++:局部变量能否和全局变量重名
2014/03/03 面试题
Python装饰器的练习题
2021/11/23 Python
关于mysql中时间日期类型和字符串类型的选择
2021/11/27 MySQL
唤醒紫霞仙子,携手再游三界!大话手游X《大话西游》电影合作专属剧情任务
2022/04/03 其他游戏
Win11自动黑屏怎么办 Win11自动黑屏设置教程
2022/07/15 数码科技