ErrorBoundary

EerrorBoundary是16版本出来的,有人问那我的15版本呢,我不听我不听,反正我用16,当然15有unstable_handleError

关于ErrorBoundary官网介绍比较详细,这个不是重点,重点是他能捕捉哪些异常。

  • 子组件的渲染
  • 生命周期函数
  • 构造函数
 1class ErrorBoundary extends React.Component {
 2  constructor(props) {
 3    super(props);
 4    this.state = { hasError: false };
 5  }
 6
 7  componentDidCatch(error, info) {
 8    // Display fallback UI
 9    this.setState({ hasError: true });
10    // You can also log the error to an error reporting service
11    logErrorToMyService(error, info);
12  }
13
14  render() {
15    if (this.state.hasError) {
16      // You can render any custom fallback UI
17      return <h1>Something went wrong.</h1>;
18    }
19    return this.props.children;
20  }
21}
22
23
24<ErrorBoundary>
25  <MyWidget />
26</ErrorBoundary>
27复制代码

开源世界就是好,早有大神封装了react-error-boundary 这种优秀的库。 你只需要关心出现错误后需要关心什么,还以来个 Reset, 完美。

 1import {ErrorBoundary} from 'react-error-boundary'
 2
 3function ErrorFallback({error, resetErrorBoundary}) {
 4  return (
 5    <div role="alert">
 6      <p>Something went wrong:</p>
 7      <pre>{error.message}</pre>
 8      <button onClick={resetErrorBoundary}>Try again</button>
 9    </div>
10  )
11}
12
13const ui = (
14  <ErrorBoundary
15    FallbackComponent={ErrorFallback}
16    onReset={() => {
17      // reset the state of your app so the error doesn't happen again
18    }}
19  >
20    <ComponentThatMayError />
21  </ErrorBoundary>
22)
23复制代码

遗憾的是,error boundaries并不会捕捉这些错误:

  • 事件处理程序
  • 异步代码 (e.g. setTimeout or requestAnimationFrame callbacks)
  • 服务端的渲染代码
  • error boundaries自己抛出的错误

原文可见参见官网introducing-error-boundaries

本文要捕获的就是 事件处理程序的错误。 官方其实也是有方案的how-about-event-handlers, 就是 try catch. 但是,那么多事件处理程序,我的天,得写多少,。。。。。。。。。。。。。。。。。。。。

 1 handleClick() {
 2    try {
 3      // Do something that could throw
 4    } catch (error) {
 5      this.setState({ error });
 6    }
 7  }
 8复制代码

Error Boundary 之外

我们先看看一张表格,罗列了我们能捕获异常的手段和范围。

异常类型同步方法异步方法资源加载Promiseasync/await
try/catch
window.onerror
error
unhandledrejection

try/catch

可以捕获同步和async/await的异常。

window.onerror , error事件

 1 window.addEventListener('error', this.onError, true);
 2    window.onerror = this.onError
 3复制代码

window.addEventListener('error') 这种可以比 window.onerror 多捕获资源记载异常. 请注意最后一个参数是 true, false的话可能就不如你期望。

当然你如果问题这第三个参数的含义,我就有点不想理你了。拜。

unhandledrejection

请注意最后一个参数是 true

 1window.addEventListener('unhandledrejection', this.onReject, true)
 2复制代码

其捕获未被捕获的Promise的异常。

XMLHttpRequest 与 fetch

XMLHttpRequest 很好处理,自己有onerror事件。 当然你99.99%也不会自己基于XMLHttpRequest封装一个库, axios 真香,有这完毕的错误处理机制。

至于fetch, 自己带着catch跑,不处理就是你自己的问题了。

这么多,太难了。 还好,其实有一个库 react-error-catch 是基于ErrorBoudary,error与unhandledrejection封装的一个组件。

其核心如下

 1 ErrorBoundary.prototype.componentDidMount = function () {
 2        // event catch
 3        window.addEventListener('error', this.catchError, true);
 4        // async code
 5        window.addEventListener('unhandledrejection', this.catchRejectEvent, true);
 6    };
 7复制代码

使用:

 1import ErrorCatch from 'react-error-catch'
 2
 3const App = () => {
 4  return (
 5  <ErrorCatch
 6      app="react-catch"
 7      user="cxyuns"
 8      delay={5000}
 9      max={1}
10      filters={[]}
11      onCatch={(errors) => {
12        console.log('报错咯');
13        // 上报异常信息到后端,动态创建标签方式
14        new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
15      }}
16    >
17      <Main />
18    </ErrorCatch>)
19}
20
21export default 
22复制代码

鼓掌,鼓掌。

其实不然: 利用error捕获的错误,其最主要的是提供了错误堆栈信息,对于分析错误相当不友好,尤其打包之后。

错误那么多,我就先好好处理React里面的事件处理程序。 至于其他,待续。

事件处理程序的异常捕获

示例

我的思路原理很简单,使用decorator来重写原来的方法。

先看一下使用:

 1 @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
 2    async createOrder() {
 3        const data = {...};
 4        const res = await createOrder();
 5        if (!res || res.errCode !== 0) {
 6            return Toast.error("创建订单失败");
 7        }
 8        
 9        .......
10        其他可能产生异常的代码
11        .......
12        
13       Toast.success("创建订单成功");
14    }
15复制代码

注意四个参数:

  • message: 出现错误时,打印的错误
  • toast: 出现错误,是否Toast
  • report: 出现错误,是否上报
  • log: 使用使用console.error打印

可能你说,这这,消息定死,不合理啊。我要是有其他消息呢。 此时我微微一笑别急, 再看一段代码

 1 @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
 2    async createOrder() {
 3        const data = {...};
 4        const res = await createOrder();
 5        if (!res || res.errCode !== 0) {
 6            return Toast.error("创建订单失败");
 7        }
 8       
 9        .......
10        其他可能产生异常的代码
11        .......
12        
13       throw new CatchError("创建订单失败了,请联系管理员", {
14           toast: true,
15           report: true,
16           log: false
17       })
18       
19       Toast.success("创建订单成功");
20
21    }
22复制代码

是都,没错,你可以通过抛出 自定义的CatchError来覆盖之前的默认选项。

这个methodCatch可以捕获,同步和异步的错误,我们来一起看看全部的代码。

类型定义

 1export interface CatchOptions {
 2    report?: boolean;
 3    message?: string;
 4    log?: boolean;
 5    toast?: boolean;
 6}
 7
 8// 这里写到 const.ts更合理
 9export const DEFAULT_ERROR_CATCH_OPTIONS: CatchOptions = {
10    report: true,
11    message: "未知异常",
12    log: true,
13    toast: false
14}
15复制代码

自定义的CatchError

 1import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
 2
 3export class CatchError extends Error {
 4
 5    public __type__ = "__CATCH_ERROR__";
 6    /**
 7     * 捕捉到的错误
 8     * @param message 消息
 9     * @options 其他参数
10     */
11    constructor(message: string, public options: CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
12        super(message);
13    }
14}
15
16复制代码

装饰器

 1import Toast from "@components/Toast";
 2import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
 3import { CatchError } from "@util/error/CatchError";
 4
 5const W_TYPES = ["string", "object"];
 6export function methodCatch(options: string | CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
 7
 8    const type = typeof options;
 9
10    let opt: CatchOptions;
11
12    
13    if (options == null || !W_TYPES.includes(type)) { // null 或者 不是字符串或者对象
14        opt = DEFAULT_ERROR_CATCH_OPTIONS;
15    } else if (typeof options === "string") {  // 字符串
16        opt = {
17            ...DEFAULT_ERROR_CATCH_OPTIONS,
18            message: options || DEFAULT_ERROR_CATCH_OPTIONS.message,
19        }
20    } else { // 有效的对象
21        opt = { ...DEFAULT_ERROR_CATCH_OPTIONS, ...options }
22    }
23
24    return function (_target: any, _name: string, descriptor: PropertyDescriptor): any {
25
26        const oldFn = descriptor.value;
27
28        Object.defineProperty(descriptor, "value", {
29            get() {
30                async function proxy(...args: any[]) {
31                    try {
32                        const res = await oldFn.apply(this, args);
33                        return res;
34                    } catch (err) {
35                        // if (err instanceof CatchError) {
36                        if(err.__type__ == "__CATCH_ERROR__"){
37                            err = err as CatchError;
38                            const mOpt = { ...opt, ...(err.options || {}) };
39
40                            if (mOpt.log) {
41                                console.error("asyncMethodCatch:", mOpt.message || err.message , err);
42                            }
43
44                            if (mOpt.report) {
45                                // TODO::
46                            }
47
48                            if (mOpt.toast) {
49                                Toast.error(mOpt.message);
50                            }
51
52                        } else {
53                            
54                            const message = err.message || opt.message;
55                            console.error("asyncMethodCatch:", message, err);
56
57                            if (opt.toast) {
58                                Toast.error(message);
59                            }
60                        }
61                    }
62                }
63                proxy._bound = true;
64                return proxy;
65            }
66        })
67        return descriptor;
68    }
69}
70复制代码

总结一下

  1. 利用装饰器重写原方法,达到捕获错误的目的
  2. 自定义错误类,抛出它,就能达到覆盖默认选项的目的。增加了灵活性。
 1 @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
 2    async createOrder() {
 3        const data = {...};
 4        const res = await createOrder();
 5        if (!res || res.errCode !== 0) {
 6            return Toast.error("创建订单失败");
 7        }
 8       Toast.success("创建订单成功");
 9       
10        .......
11        其他可能产生异常的代码
12        .......
13        
14       throw new CatchError("创建订单失败了,请联系管理员", {
15           toast: true,
16           report: true,
17           log: false
18       })
19    }
20复制代码

下一步

啥下一步,走一步看一步啦。

不,接下来的路,还很长。 这才是一个基础版本。

  1. 扩大成果,支持更多类型,以及hooks版本。
 1 @XXXCatch
 2classs AAA{
 3    @YYYCatch
 4    method = ()=> {
 5    }
 6}
 7复制代码
  1. 抽象,再抽象,再抽象

玩笑开完了,严肃一下:

当前方案存在的问题:

  1. 功能局限
  2. 抽象不够 获取选项,代理函数, 错误处理函数完全可以分离,变成通用方法。
  3. 同步方法经过转换后会变为异步方法。 所以理论上,要区分同步和异步方案。
  4. 错误处理函数再异常怎么办

之后,我们会围绕着这些问题,继续展开。

Hooks版本

有掘友说,这个年代了,谁还不用Hooks。 是的,大佬们说得对,我们得与时俱进。 Hooks的基础版本已经有了,先分享使用,后续的文章跟上。

Hook的名字就叫useCatch

 1 const TestView: React.FC<Props> = function (props) {
 2
 3    const [count, setCount] = useState(0);
 4
 5    
 6    const doSomething  = useCatch(async function(){
 7        console.log("doSomething: begin");
 8        throw new CatchError("doSomething error")
 9        console.log("doSomething: end");
10    }, [], {
11        toast: true
12    })
13
14    const onClick = useCatch(async (ev) => {
15        console.log(ev.target);
16        setCount(count + 1);
17
18        doSomething();
19
20        const d = delay(3000, () => {
21            setCount(count => count + 1);
22            console.log()
23        });
24        console.log("delay begin:", Date.now())
25
26        await d.run();
27        
28        console.log("delay end:", Date.now())
29        console.log("TestView", this)
30        throw new CatchError("自定义的异常,你知道不")
31    },
32        [count],
33        {
34            message: "I am so sorry",
35            toast: true
36        });
37
38    return <div>
39        <div><button onClick={onClick}>点我</button></div>
40        <div>{count}</div>
41    </div>
42}
43
44export default React.memo(TestView);
45复制代码

至于思路,基于useMemo,可以先看一下代码:

 1export function useCatch<T extends (...args: any[]) => any>(callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS): T {    
 2
 3    const opt =  useMemo( ()=> getOptions(options), [options]);
 4    
 5    const fn = useMemo((..._args: any[]) => {
 6        const proxy = observerHandler(callback, undefined, function (error: Error) {
 7            commonErrorHandler(error, opt)
 8        });
 9        return proxy;
10
11    }, [callback, deps, opt]) as T;
12
13    return fn;
14} 
个人笔记记录 2021 ~ 2025