Skip to content

react 中怎么捕获异常?

Posted on:2024年8月10日 at 17:07

ErrorBoundary

EerrorBoundary 是16版本出来的,之前的 15 版本有unstable_handleError

关于 ErrorBoundary 官网介绍比较详细,它能捕捉以下异常:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>;

可以考虑直接使用开源库:react-error-boundary,对开发者来说,只需要关心出现错误后的处理。

import { ErrorBoundary } from "react-error-boundary";

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

const ui = (
  <ErrorBoundary
    FallbackComponent={ErrorFallback}
    onReset={() => {
      // reset the state of your app so the error doesn't happen again
    }}
  >
    <ComponentThatMayError />
  </ErrorBoundary>
);

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

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

其实官方也有解决方案:how-about-event-handlers, 就是 try catch.

  handleClick() {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
  }

Error Boundary 之外

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

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

try/catch

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

window.onerror , error事件

window.addEventListener("error", this.onError, true);
window.onerror = this.onError;

window.addEventListener('error') 这种可以比 window.onerror 多捕获资源记载异常.

请注意最后一个参数是 true, false的话可能就不如你期望。

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

unhandledrejection

请注意最后一个参数是 true

window.removeEventListener("unhandledrejection", this.onReject, true);

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

XMLHttpRequest 与 fetch

XMLHttpRequest 很好处理,自己有onerror事件。

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

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

其实有一个库 react-error-catch 是基于ErrorBoudary,error与unhandledrejection封装的一个组件。

其核心如下

ErrorBoundary.prototype.componentDidMount = function () {
  // event catch
  window.addEventListener("error", this.catchError, true);
  // async code
  window.addEventListener("unhandledrejection", this.catchRejectEvent, true);
};

使用:

import ErrorCatch from 'react-error-catch'

const App = () => {
  return (
  <ErrorCatch
      app="react-catch"
      user="cxyuns"
      delay={5000}
      max={1}
      filters={[]}
      onCatch={(errors) => {
        console.log('报错咯');
        // 上报异常信息到后端,动态创建标签方式
        new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
      }}
    >
      <Main />
    </ErrorCatch>)
}

export default

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

事件处理程序的异常捕获

示例

使用decorator来重写原来的方法。

先看一下使用:


   @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("创建订单失败");
        }

        .......
        其他可能产生异常的代码
        .......

       Toast.success("创建订单成功");
    }

注意四个参数:

再看一段代码

  @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("创建订单失败");
        }

        .......
        其他可能产生异常的代码
        .......

       throw new CatchError("创建订单失败了,请联系管理员", {
           toast: true,
           report: true,
           log: false
       })

       Toast.success("创建订单成功");

    }

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

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

类型定义

export interface CatchOptions {
  report?: boolean;
  message?: string;
  log?: boolean;
  toast?: boolean;
}

// 这里写到 const.ts更合理
export const DEFAULT_ERROR_CATCH_OPTIONS: CatchOptions = {
  report: true,
  message: "未知异常",
  log: true,
  toast: false,
};

自定义的CatchError

import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";

export class CatchError extends Error {
  public __type__ = "__CATCH_ERROR__";
  /**
   * 捕捉到的错误
   * @param message 消息
   * @options 其他参数
   */
  constructor(
    message: string,
    public options: CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS,
  ) {
    super(message);
  }
}

装饰器

import Toast from "@components/Toast";
import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
import { CatchError } from "@util/error/CatchError";

const W_TYPES = ["string", "object"];
export function methodCatch(
  options: string | CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS,
) {
  const type = typeof options;

  let opt: CatchOptions;

  if (options == null || !W_TYPES.includes(type)) {
    // null 或者 不是字符串或者对象
    opt = DEFAULT_ERROR_CATCH_OPTIONS;
  } else if (typeof options === "string") {
    // 字符串
    opt = {
      ...DEFAULT_ERROR_CATCH_OPTIONS,
      message: options || DEFAULT_ERROR_CATCH_OPTIONS.message,
    };
  } else {
    // 有效的对象
    opt = { ...DEFAULT_ERROR_CATCH_OPTIONS, ...options };
  }

  return function (
    _target: any,
    _name: string,
    descriptor: PropertyDescriptor,
  ): any {
    const oldFn = descriptor.value;

    Object.defineProperty(descriptor, "value", {
      get() {
        async function proxy(...args: any[]) {
          try {
            const res = await oldFn.apply(this, args);
            return res;
          } catch (err) {
            // if (err instanceof CatchError) {
            if (err.__type__ == "__CATCH_ERROR__") {
              err = err as CatchError;
              const mOpt = { ...opt, ...(err.options || {}) };

              if (mOpt.log) {
                console.error(
                  "asyncMethodCatch:",
                  mOpt.message || err.message,
                  err,
                );
              }

              if (mOpt.report) {
                // TODO::
              }

              if (mOpt.toast) {
                Toast.error(mOpt.message);
              }
            } else {
              const message = err.message || opt.message;
              console.error("asyncMethodCatch:", message, err);

              if (opt.toast) {
                Toast.error(message);
              }
            }
          }
        }
        proxy._bound = true;
        return proxy;
      },
    });
    return descriptor;
  };
}

总结一下

  1. 利用装饰器重写原方法,达到捕获错误的目的
  2. 自定义错误类,抛出它,就能达到覆盖默认选项的目的。增加了灵活性。
  @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("创建订单失败");
        }
       Toast.success("创建订单成功");

        .......
        其他可能产生异常的代码
        .......

       throw new CatchError("创建订单失败了,请联系管理员", {
           toast: true,
           report: true,
           log: false
       })
    }

下一步

  1. 扩大成果,支持更多类型,以及hooks版本。

@XXXCatch
classs AAA{
    @YYYCatch
    method = ()=> {
    }
}
  1. 抽象,再抽象,再抽象

当前方案存在的问题:

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

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

Hooks版本

Hook的名字就叫useCatch


const TestView: React.FC<Props> = function (props) {

    const [count, setCount] = useState(0);


    const doSomething  = useCatch(async function(){
        console.log("doSomething: begin");
        throw new CatchError("doSomething error")
        console.log("doSomething: end");
    }, [], {
        toast: true
    })

    const onClick = useCatch(async (ev) => {
        console.log(ev.target);
        setCount(count + 1);

        doSomething();

        const d = delay(3000, () => {
            setCount(count => count + 1);
            console.log()
        });
        console.log("delay begin:", Date.now())

        await d.run();

        console.log("delay end:", Date.now())
        console.log("TestView", this)
        throw new CatchError("自定义的异常,你知道不")
    },
        [count],
        {
            message: "I am so sorry",
            toast: true
        });

    return <div>
        <div><button onClick={onClick}>点我</button></div>
        <div>{count}</div>
    </div>
}

export default React.memo(TestView);

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

export function useCatch<T extends (...args: any[]) => any>(
  callback: T,
  deps: DependencyList,
  options: CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS,
): T {
  const opt = useMemo(() => getOptions(options), [options]);

  const fn = useMemo(
    (..._args: any[]) => {
      const proxy = observerHandler(
        callback,
        undefined,
        function (error: Error) {
          commonErrorHandler(error, opt);
        },
      );
      return proxy;
    },
    [callback, deps, opt],
  ) as T;

  return fn;
}
原文转自:https://fe.ecool.fun/topic/d27c3517-ab5a-49f5-91e9-fd973eb1cd11