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

从不使用try-catch实现的async-await语法说起错误处理

时间:2023-03-13 00:02:20 科技观察

不久之前,看到了DimaGrossman写的Howtowriteasyncawaitwithouttry-catchblocksinJavascript。看到标题的时候很好奇。我知道虽然在异步程序中不用try-catchwithasync/await也能处理错误,但是这种处理方式在async/await下效果不佳,所以我很想知道有什么比用try-catch更好的。Dima去除try-catch的方法当然是同一个套路。Dima谈到了回调地狱、Promise链并最终导致了async/await。在处理错误的时候,他不喜欢try-catch的方式,于是写了一个to(promise)来封装Promise,辅以解构语法,实现同步写入但又类似于Node的错误标准代码。摘录代码如下//to.jsexportdefaultfunctionto(promise){returnpromise.then(data=>{return[null,data];}).catch(err=>[err]);}应用示例:importtofrom”。/to.js";asyncfunctionasyncTask(cb){leterr,user,savedTask;[err,user]=awaitto(UserModel.findById(1));if(!user)returncb("Nouserfound");[err,savedTask]=awaitto(TaskModel({userId:user.id,name:"DemoTask"}));if(err)returncb("Erroroccurredwhilesavingtask");if(user.notificationsEnabled){const[err]=awaitto(NotificationService.sendNotification(user.id,"TaskCreated"));if(err)returncb("Errorwhilesendingnotification");}cb(null,savedTask);}Dima的方法让人觉得眼熟,Node的回调中不是经常这样写吗?(err,data)=>{if(err){//dealwithererror}else{//dealwithdata}}所以这个方法真的很有趣。但是回过头来看,每当这段代码遇到错误时,都会通过cb()调用将错误信息推送出去,同时中断后续流程。像这样的中断错误处理其实很适合try-catch。使用try-catch重写上面的代码要用try-catch重写上面的代码,首先要去掉to()包。这样,一旦发生错误,就需要使用Promise.prototype.catch()来捕获,或者使用try-catch来捕获awaitpromise语句。捕获到的当然是各个业务代码中的errrejected。不过要注意,上面的代码并没有直接使用err,而是使用了自定义的错误信息。因此需要将被拒绝的err进一步处理成指定的错误信息。当然,这对任何人来说都不难,比如someAsync().catch(err=>Project.reject("specifiedmessage"));然后在最外层加上try-catch。所以重写后的代码是:asyncfunctionasyncTask(cb){try{constuser=awaitUserModel.findById(1).catch(err=>Promise.reject("Nouserfound"));constsavedTask=awaitTaskModel({userId:user.id,name:"DemoTask"}).catch(err=>Promise.reject("Erroroccurredwhilesavingtask"));if(user.notificationsEnabled){awaitNotificationService.sendNotification(user.id,"TaskCreated").catch(err=>Promise.reject("Errorwhilesendingnotification"));}cb(null,savedTask);}catch(err){cb(err);}}上面的代码,从代码量上来说,相比Dima的代码并没有减少多少工作量,只是去掉了很多if(err){}构造。不习惯用try-catch的程序员找不到中断点,但是用惯了try-catch的程序员都知道一旦业务流程出错(异步代码中reject),代码会跳转到catch块处理被拒绝的值。但是,一般业务代码拒绝的信息通常是有用的。如果上面每个业务rejects的err都是错误信息,那么在Dima模式下,还是需要写if(err)returncb(err);而在try-catch模式下,就简单多了asyncfunctionasyncTask(cb){try{constuser=awaitUserModel.findById(1);constsavedTask=awaitTaskModel({userId:user.id,name:"DemoTask"});if(user.notificationsEnabled){awaitNotificationService.sendNotification(user.id,"TaskCreated");}cb(null,savedTask);}catch(err){cb(err);}}为什么?因为在Dima的模式下,if(err)实际上处理了两个业务:一是捕获中断的err,并转化为错误信息,二是通过return中断业务流程。所以当不再需要将err转换为错误信息的过程时,捕获中断然后重新引起中断的处理程序是多余的。继续改进使用函数表达式改进try-catch逻辑当然,还有改进的空间。比如try{}块中的代码比较长,会造成阅读不便,try-catch的逻辑感觉“断了”。在这种情况下,可以使用函数表达式来改进asyncfunctionasyncTask(cb){asyncfunctionprocess(){constuser=awaitUserModel.findById(1);constsavedTask=awaitTaskModel({userId:user.id,name:"DemoTask"});if(user.notificationsEnabled){awaitNotificationService.sendNotification(user.id,"TaskCreated");}returnsavedTask;}try{cb(null,awaitprocess());}catch(err){cb(err);}}如果报错了处理代码比较长,也可以写成单独的函数表达式。如果流程中每个步骤的错误处理逻辑不同怎么办?如果出现错误,不再转化为错误信息,而是具体的错误处理逻辑,我们该怎么办?想一想,我们用一个字符串来表示错误信息,以后可以使用控制台的.log()来进行处理。对于逻辑来说,最合适的表达式当然是函数表达式,最终可以通过调用asyncfunctionasyncTask(cb){asyncfunctionprocess(){constuser=awaitUserModel.findById(1).catch(err=>Promise.reject(()=>{//dealwithereroronlookingfortheuserreturn"Nouserfound";}));constsavedTask=awaitTaskModel({userId:user.id,name:"DemoTask"}).catch(err=>Promise.reject(()=>{//makingmodelerror//dealwithitreturnerr===1?"Erroroccurredwhilesavingtask":"Erroroccurredwhilemakingmodel";}));if(user.notificationsEnabled){awaitNotificationService.sendNotification(user.id,"TaskCreated").catch(err=>Promise.reject(()=>{//justprintamessagelogger.log(err);返回"Errorwhilesendingnotification";}));}returnsavedTask;}try{cb(null,awaitprocess());}catch(func){cb(func());它甚至可以处理更复杂的情况。现在你应该知道了.catch(err=>Promise.reject(xx)),其中xx是try-catch的catch块捕获的对象,所以如果不同的业务reject不同的对象,比如有的是函数(代表error处理逻辑),有的是字符串(代表错误信息),有的是数字(代表错误码)——其实只需要改catch行上的块try{//...}catch(something){switch(typeofsomething){case"string"://showmessagesomethingbreak;case"function":something();break;case"number"://lookupsomethingascode//andshowcorrelativemessagebreak;default://dealwithunknownerror}}总结我没有批评Dima的错误处理方式。有了灵感,也让我重新审视了自己一直喜欢的try-catch方式。使用哪种方式取决于适用场景、团队约定、个人喜好等多重因素。在不同的情况下,需要不同的处理方法。并不是说一个就一定比另一个好——合适的是**的!