引入
我们都知道trycatch无法捕获setTimeout异步任务中的错误,那其中的原因是什么。以及异步代码在js中是特别常见的,我们该怎么做才比较?
无法捕获的情况
functionmain(){try{setTimeout(()={thrownewError(asyncerror)},)}catch(e){console.log(e,err)console.log(continue...)}}main();
这段代码中,setTimeout的回调函数抛出一个错误,并不会在catch中捕获,会导致程序直接报错崩掉。所以说在js中trycatch并不是说写上一个就可以高枕无忧了。难道每个函数都要写吗,那什么情况下trycatch无法捕获error呢?
异步任务
1、宏任务的回调函数中的错误无法捕获上面的栗子稍微改一下,主任务中写一段trycatch,然后调用异步任务task,task会在一秒之后抛出一个错误。
//异步任务consttask=()={setTimeout(()={thrownewError(asyncerror)},)}//主任务functionmain(){try{task();}catch(e){console.log(e,err)console.log(continue...)}}
这种情况下main是无法catcherror的,这跟浏览器的执行机制有关。异步任务由eventloop加入任务队列,并取出入栈(js主进程)执行,而当task取出执行的时候,main的栈已经退出了,也就是上下文环境已经改变,所以main无法捕获task的错误。事件回调,请求回调同属tasks,所以道理是一样的。eventloop复习可以看这篇
文章
、微任务(promise)的回调
//返回一个promise对象constpromiseFetch=()=newPromise((slove)={slove();})functionmain(){try{//回调函数里抛出错误promiseFetch().then(()={thrownewError(err)})}catch(e){console.log(e,eeee);console.log(continue);}}
promise的任务,也就是then里面的回调函数,抛出错误同样也无法catch。因为微任务队列是在两个task之间清空的,所以then入栈的时候,main函数也已经出栈了。
并不是回调函数无法trycatch
很多人可能有一个误解,因为大部分遇到无法catch的情况,都发生在回调函数,就认为回调函数不能catch。不全对,看一个最普通的栗子。
//定义一个fn,参数是函数。constfn=(cb:()=void)={cb();};functionmain(){try{//传入callback,fn执行会调用,并抛出错误。fn(()={thrownewError(1);})}catch(e){console.log(error);}}main();
结果当然是可以catch的。因为callback执行的时候,跟main还在同一次事件循环中,即一个eventlooptick。所以上下文没有变化,错误是可以catch的。根本原因还是同步代码,并没有遇到异步任务。
promise的异常捕获
构造函数
先看两段代码:
functionmain1(){try{newPromise(()={thrownewError(promise1error)})}catch(e){console.log(e.message);}}functionmain(){try{Promise.ject(promiseerror);}catch(e){console.log(e.message);}}
以上两个trycatch都不能捕获到error,因为promise内部的错误不会冒泡出来,而是被promise吃掉了,只有通过promise.catch才可以捕获,所以用Promise一定要写catch啊。然后我们再来看一下使用promise.catch的两段代码:
//jectconstp1=newPromise((slove,ject)={if(1){ject();}});p1.catch((e)=console.log(p1error));
//thrownewErrorconstp=newPromise((slove,ject)={if(1){thrownewError(perror)}});p.catch((e)=console.log(perror));
promise内部的无论是ject或者thrownewError,都可以通过catch回调捕获。这里要跟我们最开始微任务的栗子区分,promise的微任务指的是then的回调,而此处是Promise构造函数传入的第一个参数,newPromise是同步执行的。
then
那then之后的错误如何捕获呢。
functionmain(){Promise.solve(true).then(()={try{thrownewError(then);}catch(e){turne;}}).then(e=console.log(e.message));}
只能是在回调函数内部catch错误,并把错误信息返回,error会传递到下一个then的回调。
用Promise捕获异步错误
constp=()=newPromise((slove,ject)={setTimeout(()={ject(asyncerror);})});functionmain(){p().catch(e=console.log(e));}main();
把异步操作用Promise包装,通过内部判断,把错误ject,在外面通过promise.catch捕获。
async/await的异常捕获
首先我们模拟一个请求失败的函数fetchFailu,fetch函数通常都是返回一个promise。main函数改成async,catch去捕获fetchFailuject抛出的错误。能不能获取到呢。
constfetchFailu=()=newPromise((solve,ject)={setTimeout(()={//模拟请求if(1)ject(fetchfailu...);})})asyncfunctionmain(){try{consts=awaitfetchFailu();console.log(s,s);}catch(e){console.log(e,e.message);}}main();
async函数会被编译成好几段,根据await关键字,以及catch等,比如main函数就是拆成三段。
fetchFailu console.log(s) catch
通过step来控制迭代的进度,比如next,就是往下走一次,从1-,异步是通过Promise.then()控制的,你可以理解为就是一个Promise链,感兴趣的可以去研究一下。关键是生成器也有一个throw的状态,当Promise的状态ject后,会向上冒泡,直到step(throw)执行,然后catch里的代码
console.log(e,e.message);
执行。明显感觉async/await的错误处理更优雅一些,当然也是内部配合使用了Promise。
更进一步
async函数处理异步流程是利器,但是它也不会自动去catch错误,需要我们自己写trycatch,如果每个函数都写一个,也挺麻烦的,比较业务中异步函数会很多。首先想到的是把trycatch,以及catch后的逻辑抽取出来。
consthandle=async(fn:any)={try{turnawaitfn();}catch(e){//dosthconsole.log(e,e.messagee);}}asyncfunctionmain(){consts=awaithandle(fetchFailu);console.log(s,s);}
写一个高阶函数包裹fetchFailu,高阶函数复用逻辑,比如此处的trycatch,然后执行传入的参数-函数即可。然后,加上回调函数的参数传递,以及返回值遵守first-error,向node/go的语法看齐。如下:
consthandleTryCatch=(fn:(...args:any[])=Promise{})=async(...args:any[])={try{turn[null,awaitfn(...args)];}catch(e){console.log(e,e.messagee);turn[e];}}asyncfunctionmain(){const[err,s]=awaithandleTryCatch(fetchFailu)();if(err){console.log(err,err);turn;}console.log(s,s);}
但是还有几个问题,一个是catch后的逻辑,这块还不支持自定义,再就是返回值总要判断一下,是否有error,也可以抽象一下。所以我们可以在高阶函数的catch处做一下文章,比如加入一些错误处理的回调函数支持不同的逻辑,然后一个项目中错误处理可以简单分几类,做不同的处理,就可以尽可能的复用代码了。
//1.三阶函数。第一次传入错误处理的handle,第二次是传入要修饰的async函数,最后返回一个新的function。consthandleTryCatch=(handle:(e:Error)=void=errorHandle)=(fn:(...args:any[])=Promise{})=async(...args:any[])={try{turn[null,awaitfn(...args)];}catch(e){turn[handle(e)];}}//.定义各种各样的错误类型//我们可以把错误信息格式化,成为代码里可以处理的样式,比如包含错误码和错误信息classDbErrorextendsError{publicerrmsg:string;publicerrno:number;constructor(msg:string,code:number){super(msg);this.errmsg=msg
db_error_msg;this.errno=code
;}}classValidatedErrorextendsError{publicerrmsg:string;publicerrno:number;constructor(msg:string,code:number){super(msg);this.errmsg=msg
validated_error_msg;this.errno=code
;}}//.错误处理的逻辑,这可能只是其中一类。通常错误处理都是按功能需求来划分//比如请求失败(00但是返回值有错误信息),比如node中写db失败等。consterrorHandle=(e:Error)={//dosomethingif(einstanceofValidatedError
einstanceofDbError){//dosthturne;}turn{code:,errmsg:unKnown};}constusualHandleTryCatch=handleTryCatch(errorHandle);//以上的代码都是多个模块复用的,那实际的业务代码可能只需要这样。asyncfunctionmain(){const[error,s]=awaitusualHandleTryCatch(fetchFail)(false);if(error){//因为catch已经做了拦截,甚至可以加入一些通用逻辑,这里甚至不用判断iferrorconsole.log(error,error);turn;}console.log(s,s);}
解决了一些错误逻辑的复用问题之后,即封装成不同的错误处理器即可。但是这些处理器在使用的时候,因为都是高阶函数,可以使用es6的装饰器写法。不过装饰器只能用于类和类的方法,所以如果是函数的形式,就不能使用了。不过在日常开发中,比如React的组件,或者Mobx的sto,都是以class的形式存在的,所以使用场景挺多的。比如改成类装饰器:
constasyncErrorWrapper=(errorHandler:(e:Error)=void=errorHandle)=(target:Function)={constprops=Object.getOwnPropertyNames(target.prototype);props.forEach((prop)={varvalue=target.prototype[prop];if(Object.prototype.toString.call(value)===[objectAsyncFunction]){target.prototype[prop]=async(...args:any[])={try{turnawaitvalue.apply(this,args);}catch(err){turnerrorHandler(err);}}}});}
asyncErrorWrapper(errorHandle)classSto{asyncgetList(){turnPromise.ject(类装饰:失败了);}}conststo=newSto();asyncfunctionmain(){consto=awaitsto.getList();}main();这种class装饰器的写法是看到
*子毅
这么写过,感谢灵感。
koa的错误处理
如果对koa不熟悉,可以选择跳过不看。koa中当然也可以用上面async的做法,不过通常我们用koa写server的时候,都是处理请求,一次