有时我们会忽略错误处理和堆栈跟踪的一些细节,但这些细节对于编写与测试或错误处理相关的库非常有用。比如这周Chai有一个很好的PR,极大的改进了我们处理stack的方式,当用户断言失败的时候,我们会给出更多的提示信息(帮助用户定位)。堆栈信息的合理处理可以让你清除无用的数据,只关注有用的,同时,当你对Errors对象及其相关属性有了更深入的了解,可以帮助你充分利用Errors。(函数)调用堆栈如何工作?在讲错误之前,首先要了解(function)调用栈的原理:当一个函数被调用时,会被压入栈顶,函数执行完毕后,会被移出栈顶堆栈。堆栈的数据结构是后进先出,称为LIFO(后进先出)。例如:functionc(){console.log('c');}functionb(){console.log('b');c();}functiona(){console.log('a');b();}a();在上面的例子中,当函数a运行时,它会被添加到栈顶。然后,当函数b在函数a内部调用函数b时,函数b会被压入栈顶。当函数c在函数b内部被调用时,它也会被压入栈顶。压入栈顶。当函数c运行时,栈中包含a、b、c(按顺序)。当函数c运行完毕后,从栈顶移除,控制流函数调用返回函数b。函数b运行完毕后,也会从栈顶移除,栈顶被移除,然后函数调用的控制流返回到函数a。***,函数a运行后也会从栈顶移除。为了在demo中更好的展示栈的行为,可以使用console.trace()在控制台输出当前的栈数据。同时要从上到下依次读取输出栈数据。functionc(){console.log('c');console.trace();}functionb(){console.log('b');c();}functiona(){console.log('a');b();}a();在NodeREPL模式下运行上面的代码将得到以下输出:Traceatc(repl:3:9)atb(repl:3:1)ata(repl:3:1)atrepl:1:1//<--Fornow随意忽略这一点以下的任何内容,这些是Node的内部结构atrealRunInThisContextScript(vm.js:22:35)atsigintHandlersWrap(vm.js:98:12)atContextifyScript.Script.runInThisContext(vm.js:24:12)atREPLServer.defaultEval(repl.js:313:29)atbound(domain.js:280:14)atREPLServer.runBound[aseval](domain.js:293:12)可以看出,当从函数c输出时,栈中包含函数a、b、c。如果在函数c运行完毕后,在函数b中输出当前栈数据,会看到函数c已经从栈顶移除。此时,堆栈中只有函数a和b.functionc(){console.log('c');}functionb(){console.log('b');C();console.trace();}functiona(){console.log('a');b();}可以看到,函数c运行完毕后,已经从栈顶移除了。Traceatb(repl:4:9)ata(repl:3:1)atrepl:1:1//<--现在可以随意忽略此点以下的任何内容,这些是Node在realRunInThisContextScript(vm.js:22:35)在sigintHandlersWrap(vm.js:98:12)atContextifyScript.Script.runInThisContext(vm.js:24:12)atREPLServer.defaultEval(repl.js:313:29)atbound(domain.js:280:14)atREPLServer.runBound[aseval](domain.js:293:12)atREPLServer.onLine(repl.js:513:10)错误对象和错误处理当程序运行时出现错误,通常会抛出一个Error对象。Error对象可以作为用户自定义错误对象继承的原型。Error.prototype对象包含以下属性:constructor-指向实例消息的构造函数-错误消息名称-错误的名称(类型)。以上是Error.prototype的标准属性。此外,不同的运行环境各有其特定的属性。在Node、Firefox、Chrome、Edge、IE10+、Opera和Safari6+等环境中,Error对象有一个stack属性,其中包含错误的堆栈跟踪。一个错误实例的堆栈跟踪包含自构造函数以来的所有堆栈结构。如果你想了解更多关于Error对象的具体属性,你可以阅读MDN上的这篇文章。为了抛出错误,您必须使用throw关键字。要捕获一个throw错误,必须使用try...catch来包含可能导致错误的代码。Catch的参数是抛出的错误实例。与Java一样,JavaScript也允许在try/catch之后使用finally关键字。处理完错误后,可以在finally块中做一些清理工作。从句法上讲,您可以使用一个try块,它后面不必跟一个catch块,但必须跟一个finally块。这意味着try语句有三种不同的形式:try...catchtry...finallytry...catch...finallyTry语句也可以嵌入try语句:try{try{thrownewError('嵌套错误');//这里抛出的错误会被它自己的`catch`子句捕获}catch(nestedErr){console.log('Nestedcatch');//Thisruns}}catch(err){console.log('Thiswillnotrun.');}你也可以在catch或finally中嵌入try语句:try{thrownewError('Firsterror');}catch(err){console.log('Firstcatchrunning');try{thrownewError('第二个错误');}catch(nestedErr){console.log('第二次捕获正在运行。');}}try{console.log('Thetryblockisrunning...');}finally{try{thrownewError('Errorinsidefinally.');}catch(err){console.log('在finally块中发现错误。');}}required重要的是要注意,在抛出错误时,可以只抛出一个简单的值而不是Error对象。虽然这看起来很酷并且被允许,但这不是推荐的做法,特别是对于一些需要处理其他代码库和框架的开发人员,因为没有标准可以参考,也没有办法知道期望什么用户。您不能相信用户会抛出Error对象,因为他们可能不会这样做,而只是抛出一个字符串或值。这也意味着很难处理堆栈信息和其他元信息。例如:functionrunWithoutThrowing(func){try{func();}抓住(e){console.log('有一个错误,但我不会抛出它。');console.log('Theerror\'smessagewas:'+e.message)}}functionfuncThatThrowsError(){thrownewTypeError('IamaTypeError.');}runWithoutThrowing(funcThatThrowsError);如果用户传递给函数runWithoutThrowing的参数抛出一个错误对象,上面的代码可以正常捕获错误。然后,如果抛出一个字符串,它会遇到一些问题:functionrunWithoutThrowing(func){try{func();}catch(e){console.log('有一个错误,但我不会抛出它。');console.log('Theerror\'smessagewas:'+e.message)}}functionfuncThatThrowsString(){throw'IamaString.';}runWithoutThrowing(funcThatThrowsString);现在第二个console.log将输出undefined。这看起来不是很重要,但是如果你需要确保Error对象有一个特定的属性或者用另一种方式处理Error对象的特定属性(比如Chai的throwsassertion),你必须做很多事情工作以保证程序运行的正确性。同时,如果抛出的对象不是Error对象,则无法获取stack属性。Errors也可以作为其他对象使用,你不必抛出它们,这就是为什么大多数回调函数都将Errors作为第一个参数的原因。例如:constfs=require('fs');fs.readdir('/example/i-do-not-exist',functioncallback(err,dirs){if(errinstanceofError){//`readdir`将抛出错误,因为该目录不存在//我们现在可以在回调函数中使用它传递的错误对象console.log('错误信息:'+err.message);console.log('看到了吗?我们可以在不使用try语句的情况下使用错误。');}else{console.log(dirs);}});***,Error对象也可以用于被拒绝的promises,这使得处理被拒绝的promises变得容易:newPromise(function(resolve,reject){reject(newError('Thepromisewasrejected.'));}).then(function(){console.log('我出错了。');}).catch(function(err){if(errinstanceofError){console.log('承诺被错误拒绝.');console.log('错误信息:'+err.message);}});处理栈部分针对支持Error.captureStackTrace的运行环境。比如Nodejs.Error.captureStackTrace的第一个参数是object,第二个可选参数是Afunction。Error.captureStackTrace会抓取堆栈信息,并在第一个参数中创建一个stack属性来存放抓取到的堆栈信息。如果提供第二个参数,该函数将被用作堆堆栈调用的结束点。因此,捕获的堆栈信息只会显示函数调用前的信息。用下面两个demo来说明。第一种,只将捕获的堆栈信息存储在一个普通对象中其中:constmyObj={};functionc(){}functionb(){//这里我们将当前堆栈跟踪存储到myObj中Error.captureStackTrace(myObj);c();}functiona(){b();}//首先我们将调用这些函数a();//现在让我们看看存储到myObj.stackconsole.log(myObj.stack)中的堆栈跟踪是什么;//这会将以下堆栈打印到控制台://atb(repl:3:7)<--因为它是在B内部调用的,所以B调用是堆栈中的最后一个条目//ata(repl:2:1)//atrepl:1:1<--Nodeinternalsbelowthisline//atrealRunInThisContextScript(vm.js:22:35)//atsigintHandlersWrap(vm.js:98:12)//atContextifyScript。Script.runInThisContext(vm.js:24:12)//atREPLServer.defaultEval(repl.js:313:29)//atbound(domain.js:280:14)//atREPLServer.runBound[aseval](domain.js:293:12)//atREPLServer.onLine(repl.js:513:10)从上面的例子可以看出,第一个先调用函数a(入栈),再调用a中的函数b(入栈及a之上),然后将b中的当前栈信息抓取到myObj中。所以,控制台输出的栈信息只包含了a和b的调用信息。现在,我们将一个函数作为第二个参数传递给Error.captureStackTrace,查看输出信息:constmyObj={};functiond(){//这里我们将当前堆栈跟踪存储到myObj//这一次我们将隐藏`b`之后的所有帧和`b`本身Error.captureStackTrace(myObj,b);}functionc(){d();}functionb(){c();}functiona(){b();}//首先我们将调用这些函数a();//现在让我们看看存储到myObj.stackconsole.log(myObj.stack)中的堆栈跟踪是什么;//这将向控制台打印以下堆栈://ata(repl:2:1)<--正如你在这里看到的,我们只在`b`被调用之前获取帧//atrepl:1:1<--Nodeinternalsbelowthisline//atrealRunInThisContextScript(vm.js:22:35)//在sigintHandlersWrap(vm.js:98:12)//在ContextifyScript.Script.runInThisContext(vm.js:24:12)//在REPLServer.defaultEval(repl.js:313:29))//在博und(domain.js:280:14)//atREPLServer.runBound[aseval](domain.js:293:12)//atREPLServer.onLine(repl.js:513:10)//atemitOne(events.js:101:20)当函数b作为第二个参数传递给Error.captureStackTraceFunction时,输出栈只包含函数b被调用前的信息(虽然Error.captureStackTraceFunction是在函数d中调用的),这就是为什么只有a在控制台输出。这种方式的好处是隐藏了一些与用户无关的内部实现细节。请参阅深入了解JavaScript错误和堆栈跟踪
