You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
通过传递给 Error 构造函数的第二个参数一个 cause 属性为一个 Error 对象,即可看到是哪个错误具体产生当前的错误,对于一些调用链路比较深的可可能存在多个异常抛出情况这个特性还是相当好用的,可以准确追踪。Error Cause 当然用自定义扩展错误也能够实现这个功能
asyncfunctiondoJob(){constrawResource=awaitfetch('//domain/resource-a').catch(err=>{thrownewError('Download raw resource failed',{cause: err});});constjobResult=doComputationalHeavyJob(rawResource);awaitfetch('//domain/upload',{method: 'POST',body: jobResult}).catch(err=>{thrownewError('Upload job result failed',{cause: err});});}try{awaitdoJob();}catch(e){console.log(e);console.log('Caused by',e.cause);}// Error: Upload job result failed// Caused by TypeError: Failed to fetch
Error 的相关 api
改变堆栈帧数
默认情况下,V8 引发的几乎所有错误都具有一个 stack 属性,该属性保存最顶层的 10 个堆栈帧,格式为字符串 at xxx
importtofrom'await-to-js';asyncfunctionasyncTask(){leterr,user,savedTask;[err,user]=awaitto(UserModel.findById(1));if(!user)thrownewCustomerError('No user found');[err,savedTask]=awaitto(TaskModel({userId: user.id,name: 'Demo Task'}));if(err)thrownewCustomError('Error occurred while saving task');if(user.notificationsEnabled){const[err]=awaitto(NotificationService.sendNotification(user.id,'Task Created'));if(err)console.error('Just log the error and continue flow');}}
对于 class 方法调用适当使用装饰器进行 catch
exportfunctionCatchAsync(errorLabel=''){returnfunction(target: unknown,key: string,descriptor: PropertyDescriptor){errorLabel=errorLabel||keyconstoriginFn=descriptor.valuedescriptor.value=asyncfunction(...rest: unknown[]){try{returnawaitoriginFn.call(this, ...rest)}catch(e){// do somethingthrowe}}}}exportfunctionCatchSync(errorLabel=''){returnfunction(target: unknown,key: string,descriptor: PropertyDescriptor){errorLabel=errorLabel||keyconstoriginFn=descriptor.valuedescriptor.value=function(...rest: unknown[]){try{returnoriginFn.call(this, ...rest)}catch(e){// do somethingthrowe}}}}classA{
@CatchAsync('1')asyncrequest(){}}
目前 React 的 Error Boundary 提供的两个生命周期只存在于 class 组件;并没有相应的 hooks 能实现类似的功能
There are no Hook equivalents to the uncommon getSnapshotBeforeUpdate, getDerivedStateFromError and componentDidCatch lifecycles yet, but we plan to add them soon.
// error = The error that was caught or `undefined` if nothing errored.// resetError = Call this function to mark an error as resolved. It's// up to your app to decide what that means and if it is possible// to recover from errors.const[error,resetError]=useErrorBoundary();
import{ErrorBoundary}from'react-error-boundary'functionErrorFallback({error, resetErrorBoundary}){return(<divrole="alert"><p>Something went wrong:</p><pre>{error.message}</pre><buttononClick={resetErrorBoundary}>Try again</button></div>)}constui=(<ErrorBoundaryFallbackComponent={ErrorFallback}><ComponentThatMayError/></ErrorBoundary>)
高阶组件
import{withErrorBoundary}from'react-error-boundary'constComponentWithErrorBoundary=withErrorBoundary(ComponentThatMayError,{FallbackComponent: ErrorBoundaryFallbackComponent,onError(error,info){// Do something with the error// E.g. log to an error logging client here},})// or
@withErrorBoundary({FallbackComponent: ErrorBoundaryFallbackComponent,onError(error,info){// Do something with the error// E.g. log to an error logging client here},})classComponentThatMayErrorextendsComponent{}constui=<ComponentWithErrorBoundary/>
前言
人无完人,所以代码总会出异常的,异常并不可怕,关键是怎么处理
什么是异常
程序发生了意想不到的情况,影响到了程序的正确运行
从根本上来说,异常就是一个普通的对象,其保存了异常发生的相关信息,比如错误码、错误信息等。以 JS 中的标准内置对象 Error 为例,其标准属性有 message。许多宿主环境额外增加了 filename 和 stack 等属性
错误只有被 throw,才会产生异常,不被抛出的错误不会产生异常。比如直接
new Error()
甚至打印 Error 但是不 throw,也是不会产生异常异常的分类
编译时异常
源代码在编译成可执行代码之前产生的异常,无需执行即有异常。编译、语法解析发生错误。编译型语言对于这种很常见的,但是解析型的 js 也是会有编译型异常。通常是非合法的 js 语句、ts 编译报错
代码本身不会执行就抛异常,不会处理到打印 1 的阶段。这种情况通常不会有实际影响,因为 babel/ts 等工具处理时就会直接报错。除非不经编译直接写代码,例如有时候我们直接写在 html 中写的一些代码
运行时异常
代码被执行之后产生的异常。这些通常是很难提前发现的,因为代码实际运行中会遇到。比较常见的如
TypeError: Cannot read properties of undefined
这样的读取了undefined
的属性。运行时异常对比编译时异常的特点是代码执行到异常代码前都是会正常执行的执行到
a.b.c
前的打印能成功,异常抛出后后面的语句就不能执行了。运行时异常即可是这种引擎层面抛出的也可以是代码手动抛出的而上面说的编译时异常,即使异常语句前的正常语句也是不会执行
异常传播
异常抛出就像事件冒泡一样具有传递性。如果一个异常没有被 catch,它会沿着函数调用栈一层层传播直到栈空。
异常会不断传播直到遇到第一个 catch。 如果都没有捕获,会抛出类似 unCaughtError,表示发生了一个异常,未被捕获的异常通常会被打印在控制台上
error 对象
Error
本身作为函数直接调用和被 new 调用的效果是一样的javascript 规范中总共有 8 中错误类型构造函数
Error 是错误的基类,其他类型都继承 Error 这个类
默认的 error 对象只有一个 message 信息,很多时候对于错误的细分是很不好使,一般可以通过扩展这个错误对象,抛异常时抛出自定义的错误对象,在异常处理或时实现更精细化的处理
一种常见的应用就是在 axios 处理的异常中抛出一个扩展的 ApiError 对象,传递错误信息、错误等,在错误处理时对于这种错误进行特殊处理。如自定义上报、catch 住不作为 js 异常上报。不进行这种处理的话平时比较常见的情况就是会造成 slardar 的中 js 错误部分会有很多 axios 抛出的噪音
除了扩展错误对象,目前有一个处于 stage 4 的 Error Cause 提案 https://github.com/tc39/proposal-error-cause。这个提案也是由阿里推进的国内的首个es提案
通过传递给 Error 构造函数的第二个参数一个 cause 属性为一个 Error 对象,即可看到是哪个错误具体产生当前的错误,对于一些调用链路比较深的可可能存在多个异常抛出情况这个特性还是相当好用的,可以准确追踪。Error Cause 当然用自定义扩展错误也能够实现这个功能
Error 的相关 api
默认情况下,V8 引发的几乎所有错误都具有一个 stack 属性,该属性保存最顶层的 10 个堆栈帧,格式为字符串
at xxx
Error.stackTraceLimit
属性指定堆栈跟踪收集的堆栈帧数。默认值为10
,可以设置为任何有效的 JavaScript 数值。 更改将影响值更改后捕获的任何堆栈跟踪。如果设置为非数字值,或设置为负数,则堆栈跟踪将不会捕获任何帧这个 API 可以给自定义对象追加 stack 属性,达到模拟 Error 的效果,追加的 stack 表示调用
Error.captureStackTrace()
的代码中的位置的字符串。需要注意的是
stack
属性对于不同浏览器的格式是不一致的,通常而言监控 sdk 会统一做处理这个方法支持传递一个
constructorOpt
参数,表示所有constructorOpt
以上的帧,包括constructorOpt
,都将从生成的堆栈跟踪中省略。具体的差异如下使用这个参数可以用于调用栈过深时隐藏深层次的一些调用细节
还原错误也是利用了 error 对象的 stack 属性。可以使用
stacktracey
和source-map
实现根据错误堆栈还原到实际发生错误的代码线上代码经过压缩后一般只有 1 行,对于定位原始错误是很困难的。并且默认的
e.stack
属性是个字符串,可以借助stacktracey
进行解析并结合source-map
进行反解自动、手动抛出
异常可手动抛出也可自动抛出
自动抛出:代码执行报错由引擎抛出。这种由于逻辑缺失容错造成的自动抛出错误应该是要尽最大程度杜绝并防范的
手动抛出:直接调用
throw
那什么时候应该手动抛出异常呢?一个指导原则就是已经预知到程序不能正确进行下去了。
抛出异常还是返回特定错误信息
对于上面提到可预知的异常需要终止流程,也可以使用抛出异常或者返回特定数据来让调用方感知。
好处,调用方无需判断返回值,抛出异常默认就不会走后面的逻辑代码了。常见于 axios 对于 code 非 0 的异常抛出处理并自定义上报。再结合上面提到的扩展 error 对象,可以在监控上报前判断属于特定错误不作为 js 上报,避免网络异常造成的 js 错误增加噪音
如果上述的代码不抛出异常而是直接返回 res 的话,每一处调用就都要手动判断 code。接口 http 返回 http code 200 而响应体 code 不等于 0 也属于不抛异常而是返回特定信息的方式
异常处理
同步、异步
try-catch 作为 JavaScript 中处理异常的一种标准方式,如果 try 块中的任何同步代码发生了错误,就会立即退出代码执行过程,然后执行 catch 块。此时 catch 块会接收到一个包含错误信息的对象。try-catch 使用时也可以搭配 finnally 使用。 finally 一经使用,其代码无论如何都会执行。对于异步调用可封装成 promise 的 catch 方法进行调用或借助 async/await 语法糖使用 try/catch
可能到处 try catch 确实不是一种优雅的方式,可以进行适当的封装
Promise catch 小细节
then(f1,f2) vs then(f1).catch(f2)
绝大多数情况下是相同的。
区别在于第一种写法 f2 无法捕获 f1 中的异常。第二种写法 f2 能捕获 f1 中的异常
全局兜底
对于无需手动捕获或者没有捕获的异常最终会抛到全局。通过全局
error
和unhandledrejection
进行监听并处理。监听全局异常和未捕获的 Promise 异常并进行相关处理window.onerror
和window.addEventListener error
的区别window.onerror
函数返回 true 可以阻止执行默认事件处理函数(即控制台没有 error 打印出来)window.addEventListener error
若为捕获阶段,则可额外捕获静态资源的加载错误。window.onerror
则无法捕获静态资源的加载错误React 中的异常
白屏异常
上述提到的是同步代码报错,异步代码的报错是不会产生页面白屏,只是会产生一些 console 中的 error。同理,因为事件回调函数的处理不是在 React 处理阶段(初始化或者事件处理
setState
驱动 react 进行下次渲染的),所以事件处理函数中的报错同样不会触发白屏Error Boundary
既然白屏问题如此严重,必须要有一种方式帮助开发者来感知 React 中的白屏问题。 于是 React16 就有了
Error Boundary
来用来捕获渲染时错误的概念,在 React 新增了两个生命周期componentDidCatch
和static getDerivedStateFromError
用于捕获渲染时的错误,也仅能捕获上面提到的白屏异常(如异步错误等也是没有办法被捕获到),也就是说如果我们在Error Boundary
中捕获到错误并上报,这个错误通常是非常严重的。Error Boundary
只可用于捕获子组件中发生的异常(自身出现渲染错误也是无法捕获的)用于出错时去执行的副作用代码,比如错误上报、错误兜底等
在出错后触发,改函数返回的值能进行 setState 更新,触发一次重新 render 来渲染错误时的 fallback 组件。如果这次渲染仍然出现渲染错误,页面仍然会白屏,而不是执行类似 render error -> getDerivedStateFromError -> render error 这样的死循环
`
static getDerivedStateFromError
渲染阶段调用的,所以不允许出现副作用componentDidCatch
【commit】阶段被调用,所以允许出现副作用目前 React 的 Error Boundary 提供的两个生命周期只存在于 class 组件;并没有相应的 hooks 能实现类似的功能
但是有一个比较有趣的是,Preact 提供了相应的 hook useErrorBoundary去实现 Error Boundary。preact 中的
useErrorBoundary
的功能和getDerivedStateFromError
、componentDidCatch
是一模一样的用法也是非常简单,子组件触发异常会触发函数组件的 render 并且 error 是对应的错误信息,并且还提供了对应的 resetError 去重置错误。至于为何 Preact 能先于 React 支持功能,原因在于对于 Preact 的实现来说,它的函数组件和 class 组件都是实例化成一样的实例,函数组件的 hook 中直接定义
componentDidCatch
进行处理,componentDidCatch 捕获到错误后通过setState
设置错误对象驱动下一次的 render 来拯救白屏虽然这是一个 react 的 Error Boundary 只存在于 class 组件,但是对于子组件是函数组件的情况下,相关 hooks 的异常(
useEffect
、useLayoutEffect
)一样是能捕获到的实践
这么基础常用的 error-boundary 通常来说不需要我们手动去搞。开源社区已经有了成熟的封装解决方案react-error-boundary。它基于 React 提供的 error boundary 能力提供了开箱即用的功能,使用的时候只需要将我们的组件作为
ErrorBoundary
的子组件传入即可,并且 ErrorBoundary 还提供 FallbackComponent 属性供出错时渲染 fallback 内容、错误恢复等许多更进阶的功能。并且也提供了 HOC 的方式供使用Error Boundary 包子组件
高阶组件
在需要使用的地方对我们的组件进行一层包装即可。这时候可能会一种需求,手动包一层太麻烦了,为啥 react 不提供一个配置字段每个组件自带 error boundary 呢?
万能的开源社区也有人通过 babel 插件实现了这个能力babel-plugin-transform-react-error-boundary
通过配置一个自定义的 Error Boundary 路径,即可实现所有的组件包一个 ErrorBoundary 了,再结合
react-error-boundary
一顿操作,页面再也不会白屏了。具体实现就是通过 babel 实现以下这样的转换上面提到
Error boundaries
是不支持 ssr 场景的,所以又有人做了一个针对 ssr 的 babel 插件babel-plugin-transform-react-ssr-try-catch。 通过对 render 函数进行 trycatch 实现类似的功能
实现更多的功能
Error Boundary 除了用于捕获错误,这个特性也可以用来实现 React Suspense 相关的功能
Vue 中的异常
vue 提供了 4 个异常处理的 API,分别是
errorHandler,errorCaptured,renderError,warnHandler
。errorHandler
我们最常用的是全局配置中注册的
errorHandler
,例如异常上报场景,可用如下代码:errorHandler
可以捕获render
(vue 模板)、生命周期钩子、watch 回调、methods 方法等函数内的同步代码异常,info 参数会接收到报错函数类型(render/mounted/...);如果这些函数返回 promise ,则 promise 异常也会被捕获;errorCaptured
errorCaptured
入参和errorHandler
一样,它是 vue 组件的钩子函数,作用是捕获来自后代组件(注意不包含本组件)的错误。 vue 中的错误传播规则可以总结为,从子到父传播,依次触发各组件的errorCaptured
钩子,若某errorCaptured
返回 false,则停止传播,否则最终会传播到全局的errorHandler
;使用场景:我们可以在组件库等场景使用
errorCaptured
,捕获内部异常并上报,从而避免和业务代码报错混淆;renderError
renderError
只在开发者环境下工作,当render
函数报错时,其错误将会作为第二个参数传递到renderError
,renderError
返回的vnode
将会被渲染。使用场景:
renderError
可用于开发环节实时把组件错误渲染到页面;warnHandler
warnHandler
和errorHandler
一样是全局配置项,但warnHandler
只在开发者环境下生效,用于捕获 vue 告警。使用场景:一般情况下开发者直接在控制台查看 warn,所以
warnHandler
使用场景非常有限。参考
React,优雅的捕获异常 - 掘金
精读《React Error Boundaries》
React:Suspense 的实现与探讨
The text was updated successfully, but these errors were encountered: