JavaScript / React的window.onerror被触发两次。

14

我希望在我的React应用程序中全局捕获错误。

但每次错误被捕获/转发两次到我的注册函数。

示例代码:

window.onerror = (msg, url, lineNo, columnNo, error) => {
console.log(msg)
  alert(msg)
}

class TodoApp extends React.Component {
  constructor(props) {
    super(props)


  }

  render() {

    return (
      <button onClick={(e)=>{
        console.log("clicked")
        null.bla
      }}>
        Create an error

      </button>
    )
  }
}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))

这里有一个JS-fiddle:https://jsfiddle.net/dmxur0rc/4/

控制台仅显示了一个“clicked”日志,因此不是按钮触发两次,而是错误事件。

8个回答

7

这是一个与错误边界实现相关的已知React错误。

点击此处查看更多信息。


有趣 - 以前从未遇到过这种情况。 - Dave Newton

7
我找到了一个基本解决方案,可以在所有情况下使用。
事实证明,错误对象在所有调用中都是相同的,您可以设置某些内容以完全匹配它们,或者只需附加自定义属性到错误对象即可...
诚然,这可能只适用于window.addEventListener('error',function ...),因为您将得到真正的错误对象作为参数,而不是window.onerror = function ...,后者只获取数据部分,例如message&lineNumber,而不是真正的错误。

This is basically how I'm using it:

window.addEventListener('error', function (event) {
  if (event.error.hasBeenCaught !== undefined){
    return false
  }
  event.error.hasBeenCaught = true
  // ... your useful code here
})

如果同一错误被调用两次,它将在到达您有用的代码之前退出,每个错误仅执行一次有用的代码。

5

我已经更新了JS Fiddle以在监听器中返回true,但它仍然会产生两个警报。 - fancy
1
@fancy 你在 JSFiddle 之外试过吗(你可能需要生成一个真正的错误)? - Dave Newton
是的,我在我的真实项目中有这个问题,并在JSFiddle中重新创建了它。 你有什么想法可以帮助我调查/缩小范围吗? - fancy
@fancy 我没有。我注意到,如果我不发出警报或仅记录错误,则不再打印两次/可能启动该错误。另外,请检查不同的浏览器,我只使用 Chrome。 - Dave Newton
对我也没用,但是@Cameron Brown提供的解决方案在我的情况下起作用了。 - Eugene

5
如其他答案所述,问题出在React的DEV模式中。在该模式下,它重新抛出所有异常以“改善调试体验”。

我发现这里有4种不同的错误情况

  1. 普通的JS错误(例如来自事件处理程序,就像在问题中一样)。

    这些错误会被React的invokeGuardedCallbackDev发送到window.onerror 两次

  2. render期间发生的JS错误,并且组件树中没有React的错误边界

    与情况1相同。

  3. render期间发生的JS错误,并且组件树中有一个错误边界

    这些错误会被invokeGuardedCallbackDev发送到window.onerror 一次,但也会被错误边界componentDidCatch捕获。

  4. 未处理的promise内部的JS错误。

    这些错误不会发送到window.onerror,而是发送到window.onunhandledrejection。这只会发生一次,所以没有问题。

我的解决方法

window.addEventListener('error', function (event) {
    const { error } = event;
    // Skip the first error, it is always irrelevant in the DEV mode.
    if (error.stack?.indexOf('invokeGuardedCallbackDev') >= 0 && !error.alreadySeen) {
        error.alreadySeen = true;
        event.preventDefault();
        return;
    }
    // Normal error handling.
}, { capture: true });

请问能否提供这4种错误场景的参考资料? - goamn
我通过实验找到了这些场景,没有参考文献。 - Monsignor

1
我使用这个错误边界来处理React和全局错误。以下是React文档中的一些建议:
  • 错误边界是React组件,可以在它们的子组件树中任何地方捕获JavaScript错误,记录这些错误,并显示一个替代UI,而不是崩溃的组件树。
  • 如果一个类组件定义了static getDerivedStateFromError()或componentDidCatch()生命周期方法中的任意一个(或两个),则它将成为一个错误边界。
  • 只有在恢复意外异常时才使用错误边界;不要试图将它们用于控制流程。
  • 请注意,错误边界仅会捕获树下面的组件中的错误;错误边界无法捕获其自身内部的错误。


class ErrorBoundary extends React.Component {

      state = {
        error: null,
      };
      lastError = null;

      // This lifecycle is invoked after an error has been thrown by a descendant component. It receives the error that was thrown as a parameter and should return a value to update state.
      static getDerivedStateFromError(error) {
        // Update state so the next render will show the fallback UI.
        return {
          error,
        };
      }

      componentDidMount() {
        window.onerror = (msg, url, line, column, error) => {
          this.logError({
            error,
          });
        };
      }

      // getDerivedStateFromError() is called during the “render” phase, so side-effects are not permitted. For those use cases, use componentDidCatch() instead.

      // This lifecycle is invoked after an error has been thrown by a descendant component. It receives two parameters:

      // error - The error that was thrown.
      // info - An object with a componentStack key containing
      componentDidCatch(error, info) {
        // avoid calling log error twice
        if (this.lastError && this.lastError.message === this.state.error.message) {
          return true;
        }
        // Example "componentStack":
        //   in ComponentThatThrows (created by App)
        //   in ErrorBoundary (created by App)
        //   in div (created by App)
        //   in App
        // logComponentStackToMyService(info.componentStack);
        this.logError({
          error,
          info,
        });
      }

      async logError({
        error,
        info
      }) {
        this.lastError = error;

        try {
          await fetch('/error', {
            method: 'post',
            body: JSON.stringify(error),
          });
        } catch (e) {}
      }

      render() {
        if (this.state.error) {
          return  display error ;
        }

        return this.props.children;
      }

}


0
另一种方法是将上一个错误消息存储在状态中,并在第二次发生时进行检查。
export default MyComponent extends React.Component{
  constructor(props){
    super(props);

    this.state = {message: null};
  }

  componentDidMount(){
     const root = this;
     window.onerror = function(msg, url, line, column, error){
        if(root.state.message !== msg){
           root.setState({message: msg});

           // do rest of the logic
        }
     }
  }
}

无论如何,使用React错误边界是个好主意。您可以在错误边界组件中实现全局JavaScript错误处理。在那里,您既可以捕获JS错误(使用window.onerror),也可以捕获React错误(使用componendDidCatch)。

0

我的解决方法:应用防抖技术(在TypeScript中):

import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

let errorObserver: any;

window.onerror = (msg, url, lineNo, columnNo, error) => {
  if (!errorObserver) {

    new Observable(observer => {
      errorObserver = observer
    }).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
      .pipe(distinctUntilChanged()) // only emit if value is different from previous value
      .subscribe(handleOnError)
  }

  errorObserver.next(
    {
      msg,
      url,
      lineNo,
      columnNo,
      error
    }
  )

  return true
}

const handleOnError = (value: any) => {
  console.log('handleOnError', value)
}

enter image description here


-2

这看起来可能会因在JSFiddle上运行而触发两次。在正常的构建过程中(使用webpackbabel),像那样带有脚本错误的代码应该无法转译。


它在我的真实项目中转译得很好,我只是在JSFiddle上复制了它。 - fancy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接