当前位置: 首页 > Web前端 > JavaScript

精读《捕获所有异步 error》

时间:2023-03-26 23:59:27 JavaScript

成熟产品对稳定性要求高。只有前端需要做大量的监控和报错,后端更是如此。一个没有考虑到的异常可能会导致数据错误、服务雪崩、内存溢出等,问题小到每天处理异常,大到导致上线失败。假设代码逻辑没有错误,那么剩下的就是异常错误了。由于任何服务或代码都可能有外部调用,只要外部调用存在不确定性,就可能出现代码异常,因此捕获异常是一项非常重要的基本功。所以本周精读HowtoavoiduncaughtasyncerrorsinJavascript,看看JS是如何捕获异步异常错误的。概述之所以关注异步异常是因为捕获同步异常非常简单:try{;(()=>{thrownewError('err')})()}catch(e){console.log(e)//caught}但异步错误不能直接捕获,不直观:log(e)}原因是异步代码不是在trycatch的上下文中执行的,唯一的同步逻辑是创建一个异步函数,所以异步函数中的错误是捕捉不到的。要在异步函数中捕获异常,您可以调用.catch,因为异步函数返回一个Promise:;(async()=>{thrownewError('err')})().catch((e)=>{console.log(e)//caught})当然也可以在函数体中直接使用trycatch:;(async()=>{try{thrownewError('err')}catch(e){console.log(e)//caught}})()类似的,如果在循环体中捕获到异常,应该使用Promise.all:try{awaitPromise.all([1,2,3].map(async()=>{thrownewError('err')}))}catch(e){console.log(e)//caught}也就是说可以捕获await修饰的promise抛出的异常通过尝试捕捉。但是不代表写了await就可以捕获到异常。一种情况是Promise包含异步:newPromise(()=>{setTimeout(()=>{thrownewError('err')//uncaught},0)}).catch((e)=>{console.log(e)})在这种情况下,必须以拒绝模式抛出异常才能被捕获:newPromise((res,rej)=>{setTimeout(()=>{rej('err')//caught},0)}).catch((e)=>{console.log(e)})另一种情况是这个await没有执行:constwait=(ms)=>newPromise((res)=>setTimeout(res,ms));(async()=>{try{constp1=wait(3000).then(()=>{thrownewError('err')})//未捕获的awaitwait(2000).then(()=>{thrownewError('err2')})//caughtawaitp1}catch(e){console.log(e)}})()p1等待3s并抛出异常,但是因为2s后抛出了err2异常,打断了代码的执行,所以awaitp1不会执行,也不会捕获到这个异常。而且有意思的是,如果换个场景,提前执行p1,然后1s之后awaitp1,那么异常就会从uncatchable变成catchable,那么浏览器会怎么处理呢?constwait=(ms)=>newPromise((res)=>setTimeout(res,ms));(async()=>{try{constp1=wait(1000).then(()=>{thrownewError('err')})awaitwait(2000)awaitp1}catch(e){console.log(e)}})()结论是1s后浏览器会抛出未捕获的异常,但是1s后这个未捕获的异常只是消失并成为捕获的异常。这个行为很奇怪,当程序复杂的时候很难排错,因为并行Promise推荐使用Promise.all:awaitPromise.all([wait(1000).then(()=>{thrownewError('err')}),//p1wait(2000),])另外,Promise错误会随着Promise链一起传递,所以建议将Promise中的多个异步行为重写为多个链,并在最后捕获错误。还是之前的例子,Promise不能捕获内部异步错误:newPromise((res,rej)=>{setTimeout(()=>{throwError('err')},1000)//1}).catch((error)=>{console.log(error)})但是如果写成一个PromiseChain,它可以被捕获:newPromise((res,rej)=>{setTimeout(res,1000)//1}).then((res,rej)=>{throwError('err')}).catch((error)=>{console.log(error)})原因是内部多重异步嵌套被PromiseChain替代,这样多个异步行为就会被拆解成PromiseChain对应的同步行为,Promises可以捕获它们。最后,在DOM事件监听器中抛出的错误无法捕获:document.querySelector('button').addEventListener('click',async()=>{thrownewError('err')//uncaught})synchronously相同:document.querySelector('button').addEventListener('click',()=>{thrownewError('err')//uncaught})只能在函数体中用trycatch捕获。精读我们在开头提到,我们需要监控所有的异常。仅通过trycatch和then捕获同步和异步错误是不够的,因为这些是本地错误捕获方法。当我们不能保证所有的代码都处理了异常时,就需要全局的异常监控,一般有两种方法:window.addEventListener('error')window.addEventListener('unhandledrejection')error可以监听所有的同步和异步运行时错误,但是不能监听语法、接口和资源加载错误。而unhandledrejection可以监听Promise中抛出的没有被.catch捕捉到的错误。在具体的前端框架中,有些问题也可以通过框架提供的错误监控解决方案来解决,比如React的ErrorBoundaries,Vue的errorhandler,一个是UI组件层面的,一个是全局的。回过头来看,js本身提供的trycatcherrorcapture非常有效。之所以抓不到错误,往往是异步造成的。但是,大多数异步错误都可以通过await解决。唯一需要注意的是,await只支持一层错误监控,或者说是一条链的错误监控。例如,这个例子可以监控错误:也就是说,只要这条链是等待的,那么最外层的trycatch就可以捕捉到异步错误。但是如果有一层asynchrony脱离了await,那么就无法捕获了:asyncfunctionfunc2(){setTimeout(()=>{throwError('error')//uncaught})}对于这个问题,原文中还提供了例如Promise.all、chainedPromise、.catch等方法来解决,所以只要在写代码的时候注意异步处理,就可以使用trycatch来捕获这些异步错误.总结异步错误的处理。如果还有其他没有考虑到的情况,欢迎留言补充。讨论地址是:Jingdu《捕获所有异步 error》·Issue#350·dt-fe/weekly想参与讨论的请戳这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)