当前位置: 首页 > 科技观察

JavaScript错误处理和堆栈跟踪分析_0

时间:2023-03-12 12:00:26 科技观察

有时我们会忽略错误处理和堆栈跟踪的一些细节,但这些细节对于编写与测试或错误处理相关的库非常有用。比如这周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会被压入栈顶,当函数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();在Node的REPL模式下运行上述代码将产生以下输出:Traceatc(repl:3:9)atb(repl:3:1)ata(repl:3:1)atrepl:1:1//<--Fornowfeelfreetoignoreanythingbelowthispoint,theseareNode'sinternalsatrealRunInThisContextScript(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](域.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运行完毕后,已经从栈顶移除remove.Traceatb(repl:4:9)ata(repl:3:1)atrepl:1:1//<--现在可以随意忽略低于这一点的任何事情,这些是Node的内部真实运行InThisContextScript(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)错误对象和错误处理当程序运行过程中出现错误时,通常会抛出一个Error对象。Error对象可以作为用户定义的错误对象的原型继承。Error.prototype对象包含以下属性:constructor——指向实例message的构造函数——错误信息name——错误的名称(类型)。以上是Error.prototype的标准属性。此外,不同的运行环境有其特定的属性。在Node、Firefox、Chrome、Edge、IE10+、Opera和Safari6+等环境中,Error对象具有包含错误堆栈跟踪的堆栈属性。Error实例的堆栈跟踪包含自构造函数以来的所有堆栈结构。如果你想了解更多关于Error对象的具体属性,你可以阅读MDN上的这篇文章。为了抛出错误,您必须使用throw关键字。为了捕获抛出的错误,您必须使用try...catch来包含可能抛出错误的代码。Catch的参数是抛出的错误实例。与Java一样,JavaScript也允许在try/catch之后使用finally关键字。处理错误后,您可以在finally块中进行一些清理。从语法上讲,您可以在没有catch块的情况下使用try块,但可以使用finally块。这意味着try语句有三种不同的形式:try...catchtry...finallytry...catch...finallyTry语句也可以嵌在try语句中:try{try{thrownewError('Nestederror.');//这里抛出的错误会被itsown`catch`子句捕获}catch(nestedErr){console.log('Nestedcatch');//Thisruns}}catch(err){console.log('Thiswillnotrun.');}你也可以在catch或finally中嵌入try语句:try{console.log('Thetryblockisrunning...');}finally{try{thrownewError('Errorinsidefinally.');}catch(err){console.log('Caughtanerrorinsidethefinallyblock.');}}重要的是要注意,当抛出错误时,可能只抛出一个简单的值而不是一个错误对象。虽然这看起来很酷并且是允许的,但它不是不推荐的做法,特别是对于一些需要处理其他人代码的库和框架开发人员,因为没有标准可参考,也没有办法知道要做什么期待用户。您不能相信用户会抛出Error对象,因为他们可能不会这样做,而只是抛出一个字符串或值。这也意味着很难处理堆栈信息和其他元信息。例如:functionrunWithoutThrowing(func){try{func();}catch(e){console.log('有错误,但我不会抛出它。');console.log('Theerror\'smessagewas:'+e.message)}}functionfuncThatThrowsError(){thrownewTypeError('IamaTypeError.');}runWithoutThrowing(funcThatThrowsError);如果用户传递给函数runWithoutThrowing的参数抛出一个错误对象,上面的代码可以正常捕获错误。然后,如果抛出一个字符串,就会出现一些问题:console.log('Theerror\'smessagewas:'+e.message)}}functionfuncThatThrowsString(){throw'IamaString.';}runWithoutThrowing(funcThatThrowsString);现在第二个console.log将输出undefined。这可能看起来不是很重要,但是如果你需要确保Error对象有一个特定的属性或者使用另一种方式来处理Error对象的特定属性(例如,Chai的throws断言方法),你要做很多事情工作以确保程序的正确运行。同时,如果抛出的对象不是Error对象,则无法获取stack属性。Errors也可以作为其他对象使用,你也不必抛出它们,这就是为什么大多数回调函数都将Errors作为第一个参数的原因。例如:constfs=require('fs');fs.readdir('/example/i-do-not-exist',functioncallback(err,dirs){if(errinstanceofError){//`readdir`willthrowanerrorbecausethatdirectorydoesnotexist//Wewillnowbeabletousetheerroroobjectpassedbyitinourcallbackfunctionconsole.log('ErrorMessage:'+err.message);{console.log(dirs);}});***,Error对象也可以用于被拒绝的promises,这样可以很容易的处理被拒绝的promises:newPromise(function(resolve,reject){reject(newError('Thepromisewasrejected.'));}).then(function(){console.log('Iamanerror.');}).catch(function(err){if(errinstanceofError){console.log('Thepromisewasrejectedwithanerror.');console.log('ErrorMessage:'+错误信息);}});本节针对支持Error.captureStackTrace的运行环境,例如Nodejs.Error.captureStackTrace的第一个参数为object,第二个可选参数为function。Error.captureStackTrace会捕获堆栈信息,并创建一个堆栈第一个参数中的属性用于存储捕获的堆栈信息。如果提供了第二个参数,该函数将被用作堆栈调用的终点。因此,捕获到的堆栈信息只会显示函数调用之前的信息。用下面两个demo来说明。第一种,只将捕获的堆栈信息存储在一个普通对象中:constmyObj={};functionc(){}functionb(){//这里我们将storethecurrentstacktraceintomyObjError.captureStackTrace(myObj);c();}functiona(){b();}//Firstwewillcallthesefunctionsa();//Nowlet'sseewhatisthacktracestoredintomyObj.stackconsole.log()stack/j/这将打印以下堆栈到控制台://atb(repl:3:7)<--SinceitwascalledinsideB,theBcallistelastentryinthack//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首先被调用(压入栈),然后函数在ab中被调用(入栈a之上),然后捕获b中的当前栈信息,存入myObj中。因此,控制台信息输出的堆栈信息中只有a和b的调用。现在,我们传一个函数给Error.captureStackTrace作为第二个参数,看输出信息:constmyObj={};functiond(){//这里wewillstorethecurrentstacktraceintomyObj//这个时候wewillhidealltheframesafter`b`and`b`itselfError.captureStackTrace(myObj,b);}functionc(){d();}functionb(){c();}functiona(){b();}//首先我们将调用这些函数sa();//现在让我们看看什么是stacktracstoredintomyObj.stackconsole.log(myObj.stack);//这会将以下堆栈打印到控制台://ata(repl:2:1)<--Asyoucanseehereweonlygetframesbefore`b`wascalled//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(域.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调用前的信息(虽然在函数d中调用了Error.captureStackTraceFunction),这也是控制台只输出a的原因。这种方法的好处是隐藏了一些不相关的内部实现细节。