likes
comments
collection
share

【React错误处理】超全指南来了

作者站长头像
站长
· 阅读数 21

一、为何报错会导致渲染异常?

在React中,未捕获错误会导致DOM被卸载, 浏览器无法渲染。 为何React选择完全移除错误的DOM呢,我们可以看看官网中的这段话:

未捕获错误(Uncaught Errors)的新行为

这一改变具有重要意义,自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

我们对这一决定有过一些争论,但根据我们的经验,把一个错误的 UI 留在那比完全移除它要更糟糕。例如,在类似 Messenger 的产品中,把一个异常的 UI 展示给用户可能会导致用户将信息错发给别人。同样,对于支付类应用而言,显示错误的金额也比不呈现任何内容更糟糕。

从我的开发经验看来,出现bug的原因主要有以下两点:

① 后端返回数据异常,前端代码未兼容完全;

② 前端程序逻辑错误;

如果项目上线后,页面无法正常打开,无法执行其他操作甚至一片空白,用户的体验感是非常不好的。

【React错误处理】超全指南来了

因此,我们有必要采取一些措施来预防和处理异常/错误,避免整个页面崩溃。

二、解决方案:防bug+补救bug

【React错误处理】超全指南来了

(1) “防bug于未然”: 对后端数据进行预处理

正常情况下,前端小伙伴与后端提前沟通好状态码和数据结构,根据状态码做出不同响应即可。但是,当后端数据异常(如返回undefined, null)时,前端直接调用数组的某些方法或者对象的某些属性时就会报错。

【React错误处理】超全指南来了

  • 前端小伙伴谨记, "不要完全相信后端的数据"。*

在使用后端数据前,最好先赋默认值。

举个🌰:

// ① 解构时赋默认值 (注意:arr为null时,无法赋值成功)
// ② 使用逻辑或
const {arr = []} = data || {};

// ③ 使用可选运算符
const names = _arr?.map((item = {})=>(item?.name))).filter(Boolean);
...

在复杂的场景下,你甚至可以做更多——例如,先将后台数据进行预处理(与业务逻辑无关的数据处理),转为自身需要的结构和类型,让业务组件/逻辑更加纯粹地处理业务的同时,减少bug出现的概率。

(当然,有很多bug是前端代码自身的问题,在此不赘述预防措施了,大家可以自行思考🤔。)


然而人无完人,bug总是防不胜防,那么如何减小bug的影响呢?

(2) “亡羊补牢”之 使用常规手段捕获异常;

对于javascript而言,执行的事件主要有以下五种:同步方法、异步方法、资源加载、Promise、async...await,事件执行失败意味着程序出现bug。

幸运的是,这些异常均可通过框架(react/vue/angular等)之外的常规手段捕获到。

方法汇总

异常类型同步方法异步方法资源加载Promiseasync...await
try...catch
window.onerror
addEventListener('error')
addEventListener(“unhandledrejection”)

1. try...catch

try...catch 语句标记要尝试的语句块,并指定一个出现异常时抛出的响应。

举些🌰:

① 处理同步错误;
  • MDN Web Docs中的例子:
try {
  nonExistentFunction();
} catch (error) {
  console.error(error);
  // Expected output: ReferenceError: nonExistentFunction is not defined
  // (Note: the exact output may be browser-dependent)
}

② 处理异步错误

通常,若 try 中的异步模块产生了错误,catch 是捕获不到的。但是我们可以把 try-catch 放到异步代码中。

  • try-catch放到setTimeout内部
setTimeout(() => {
 try {
   throw new Error('error in setTimeout');
 } catch (err) {
   console.error('catch error', err);
 }
}, 200);
  • try-catch放到then内部
Promise.resolve().then(() => {
  try {
    throw new Error('error in Promise.then');
  } catch (err) {
    console.error('catch error', err);
  }
});

// 正常情况下,使用Promse自带的catch捕获异常即可
Promise.resolve()
  .then(() => {
    throw new Error('error in Promise.then');
  })
  .catch((err) => {
    console.error('Promise.catch error', err);
  });
③ 处理 async-await 的异常
  • try放在async之后
const request = async () => {
  try {
    const { code, data } = await somethingThatReturnsAPromise();
  } catch (err) {
    console.error('request error', err);
  }
};

2. window.onerror

当JavaScript运行时错误(包括语法错误)发生时,会执行window.onerror方法。

 function onError (msg, url, lineNo, columnNo, error) { 
 / * 
 * message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event
 * source:发生错误的脚本URL(字符串) 
 * lineno:发生错误的行号(数字) 
 * colno:发生错误的列号(数字) 
 * errorError对象
 */
 
   // 没有返回值或者返回值为false的时候,异常信息会通过 console.error 的方式在控制台打印
   
    return false;
 }
    window.onerror = onError

3. addEventListener('error')

当资源加载失败或无法使用时,会在Window对象触发error事件。例如:script 执行时报错。

window.addEventListener('error', (event) => {
    console.log('捕获到异常', event);
    return false;
},true); // 捕获阶段

【React错误处理】超全指南来了

4. addEventListener('unhandledrejection')

当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件。

// 捕获未处理的 promise 异常
window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});

小结

【React错误处理】超全指南来了

try...catch最为灵活,通过使用一些小技巧,可以捕获绝大部分异常,捕获到错误后可以在catch中进行更多处理;

addEventListener('error')  事件监听 js 运行时错误事件,会比 window.onerror 先触发,与onerror的功能大体类似,但可以全局捕获资源加载异常的错误

addEventListener('error')结合addEventListener('unhandledrejection'),几乎可以捕获程序中的所有错误,但主要只是提供了错误堆栈信息;

④ 当使用以上四种常规手段捕获到错误后,我们可以做许多事情。例如,在开发环境中,可将错误信息打印在浏览器控制台上、可抛出异常通知下游, 方便开发调试;在生产环境中,可上报错误日志进行错误监控,而在修复bug的过程中,我们可以做更多——结合错误边界(Error Boundary)为用户渲染一些有用的内容。


(3) “亡羊补牢”之 使用Error Boundary

【React错误处理】超全指南来了

上文提到,try...catch特别好用,但是它无法直接捕获到react组件中的所有可能发生的错误,如子组件中的错误。

此时,Error Boundary就必须得闪亮登场了~

1.Error Boundary概念

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。

错误边界最基本的实现:

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

  static getDerivedStateFromError(error) {    // 更新 state 使下一次渲染能够显示降级后的 UI    return { hasError: true };  }
  componentDidCatch(error, errorInfo) {    // 你同样可以将错误日志上报给服务器    logErrorToMyService(error, errorInfo);  }
  render() {
    if (this.state.hasError) {      // 你可以自定义降级后的 UI 并渲染      return <h1>Something went wrong.</h1>;    }
    return this.props.children; 
  }
}

使用

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

tips, 日常开发中,我们需要为不同粒度的组件运用错误边界组件,尽量减小bug的影响范围。

2.Error Boundary可用场景和不可用场景

① 错误边界起作用的场景:

  • 发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误

② 错误边界不起作用的场景:

  • 组件外的报错、异步代码的报错、事件函数中的报错、错误边界自身抛出的错误、错误边界的父组件报错、 函数组件被卸载,触发 useEffect 的销毁。

3、怎么让errorBoundary处理在生命周期之外的错误?

机智的小伙伴会发现,错误边界不能处理的许多错误,比如promise、异步代码、各种回调和事件处理程序中的错误,可以使用常规 try...catch来处理。

因此,我们先用try...catch捕获这些错误,然后在catch 语句内触发正常的 React 重新渲染,然后将这些错误重新抛出到重新渲染生命周期中

  • ① 定义异步错误抛出工具:
// 定义
import { useState } from 'react';

const useThrowAsyncError = () => {
    const [, setState] = useState();

    return (e: any) => {
        setState(() => { throw e })
    }
};

export default useThrowAsyncError;

// 使用示例
const Component = () => {
  const throwAsyncError = useThrowAsyncError();

  useEffect(() => {
    fetch('/bla').then().catch((e) => {
      // throw async error here!
      throwAsyncError(e)
    })
  })
}
  • ② 为回调函数做额外处理:
// 定义
import { useState } from 'react';

const useCallbackWithErrorHandler = (callback: (...args: any[]) => any, useErrorBoundary: boolean = false) => {
  const [, setState] = useState();

  return async (...args: any[]) => {
    try {
      await callback(...args);
    } catch (e) {
      useErrorBoundary && setState(() => { throw e });
    }
  }
};

export default useCallbackWithErrorHandler;

// 使用示例
const Component = () => {
  const onClick = () => {
    // do something dangerous here
  }
  const onClickWithErrorHandler = useCallbackWithErrorHandler(onClick);

  return <button onClick={onClickWithErrorHandler}>click me!</button>
}

三、总结

希望看到这里的小伙伴,可以从容而优雅地处理程序中出现的bug。

本文就错误处理做了详细的解析,主要内容如下:

  • 必要性:因为未被错误边界捕获的异常会导致整个react组件树被卸载,微不足道的错误都有可能导致整个页面受到破坏,并为用户渲染出一个白屏,所以预防并处理异常是必要的。

  • 预防措施:尽量减少bug出现的概率,除了减少前端自身代码的问题,最好对后端数据进行预处理再使用;

  • 事后补救方案1:使用常规手段(如try...catch、window.onerror、addEventListener('error')、addEventListener(“unhandledrejection”))捕获异常并做进一步处理,如错误上报、更新状态渲染降级UI等;

  • 事后补救方案2:结合Error Boundary为用户渲染有用的信息,避免白屏,提升用户体验感。


四、参考与感谢

【React错误处理】超全指南来了

  1. 见鬼,为何我的 ErrorBoundary(错误边界)不起作用

  2. 译文:React 错误处理:最佳实践

    原文:How to handle errors in React: full guide

  3. 喂,你的页面白了!阿里解决「前端白屏」的方案

  4. React,优雅的捕获异常

  5. 前端中 try-catch 捕获不到哪些异常和错误

  6. window.onerror 和window.addEventListener('error')的区别

  7. Throwing Error from hook not caught in error boundary · Issue #14981 · facebook/react