61

I've made a react application which is not running live, and the people that use it note that very occasionally some strange error occurs. I don't know why or what happens, and can't reproduce it.

So I'm wondering if there is a way to wrap the entire app, or parts of it, in a try/catch block so that I can send the errors to an error log on the server?

All I've read so far is that you could wrap the entire render function in a try/catch, but that would not catch any errors due to user interation right?

Flion
  • 10,468
  • 13
  • 48
  • 68
  • You have to use window.addEventListener("error") and window.addEventListener("unhandledrejection"). Check my answer for more details. – andrew.fox Mar 23 '21 at 06:02

5 Answers5

49

Edit

The below solution catches only render errors because they are sync in nature. That is how JS works, it has nothing to do with React, which is what the OP asked about.

Original post

React 16 introduced Error Boundaries and the componentDidCatch lifecycle method:

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;
  }
}

Then you can use it as a regular component:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Or you can wrap your root component with the npm package react-error-boundary, and set a fallback component and behavior.

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

const myErrorHandler = (error: Error, componentStack: string) => {
  // ...
};

<ErrorBoundary onError={myErrorHandler}>
  <ComponentThatMayError />
</ErrorBoundary>
Shining Love Star
  • 5,734
  • 5
  • 39
  • 49
  • May be worthwhile noting that errors can still creep through. I have a functional component with an error in `useEffect` which doesn't get caught by `componentDidCatch` – Frederik Petersen Sep 17 '20 at 16:42
  • 1
    However, if i also add `getDerivedStateFromError`, I am able to catch functional error there – Frederik Petersen Sep 17 '20 at 16:49
  • 5
    No, error boundary components don't seem to catch JS errors thrown in code -- only those from rendering. – mtyson Sep 28 '20 at 01:34
42

this is what I ended up using

EDIT: React 16 introduced proper ways to do this, see @goldylucks answer.

  componentWillMount() {
    this.startErrorLog();
  }

  startErrorLog() {
    window.onerror = (message, file, line, column, errorObject) => {
      column = column || (window.event && window.event.errorCharacter);
      var stack = errorObject ? errorObject.stack : null;

      //trying to get stack from IE
      if (!stack) {
        var stack = [];
        var f = arguments.callee.caller;
        while (f) {
          stack.push(f.name);
          f = f.caller;
        }
        errorObject['stack'] = stack;
      }

      var data = {
        message: message,
        file: file,
        line: line,
        column: column,
        errorStack: stack
      };

      //here I make a call to the server to log the error

      //the error can still be triggered as usual, we just wanted to know what's happening on the client side
      return false;
    };
  }
jonschlinkert
  • 10,872
  • 4
  • 43
  • 50
Flion
  • 10,468
  • 13
  • 48
  • 68
  • 1
    Hey, I tried out this code and this works on firefox but I just get "Script Error" on chrome. Any idea how to solve this? – JiN Jun 09 '17 at 05:35
  • @JiN I've encountered the same problem. It seems ([see here](https://blog.sentry.io/2016/05/17/what-is-script-error.html)) that the generic "Script Error" message occurs when there is a CORS issue. I'm getting it however, even when my scripts seem to be loaded on the same domain, so... – fraxture Jul 24 '17 at 20:58
  • @fraxture For me the issue was that I had opened developer tools in chrome.. The same code was working in firefox.. So once i exited the developer tools, it started showing the correct message. – JiN Jul 25 '17 at 07:02
  • 2
    do you just put this in the root component? – NSjonas Nov 29 '17 at 19:21
16

You can leverage React's BatchingStrategy API to easily wrap a try/catch around all of your React code. The benefit of this over window.onerror is that you get a nice stack trace in all browsers. Even modern browsers like Microsoft Edge and Safari don't provide stack traces to window.onerror.

Here's what it looks like with React 15.4:

import ReactUpdates from "react-dom/lib/ReactUpdates";
import ReactDefaultBatchingStrategy from "react-dom/lib/ReactDefaultBatchingStrategy";

let isHandlingError = false;
const ReactTryCatchBatchingStrategy = {
  // this is part of the BatchingStrategy API. simply pass along
  // what the default batching strategy would do.
  get isBatchingUpdates () { return ReactDefaultBatchingStrategy.isBatchingUpdates; },

  batchedUpdates (...args) {
    try {
      ReactDefaultBatchingStrategy.batchedUpdates(...args);
    } catch (e) {
      if (isHandlingError) {
        // our error handling code threw an error. just throw now
        throw e;
      }

      isHandlingError = true;
      try {
        // dispatch redux action notifying the app that an error occurred.
        // replace this with whatever error handling logic you like.
        store.dispatch(appTriggeredError(e));
      } finally {
        isHandlingError = false;
      }
    }
  },
};

ReactUpdates.injection.injectBatchingStrategy(ReactTryCatchBatchingStrategy);

Full writeup here: https://engineering.classdojo.com/blog/2016/12/10/catching-react-errors/

Byron Wong
  • 211
  • 3
  • 5
  • An important caveat: "And one caveat to keep in mind: React >= 15 swallows and rethrows errors internally when `NODE_ENV === "development"` so this batching strategy won’t actually make a difference in dev environments." – John Feb 17 '17 at 22:14
  • Where do you put it? Your blog just has a code snippet but not indication on how to actually use it in a big app. It would be nice to give an idea of the implementation. – james emanon Mar 19 '17 at 22:13
  • 1
    This will no longer work as of React 16. The lib import is no longer exposed – David May 26 '17 at 20:35
  • @Ben Starting with React 16, there will be an ErrorBoundary API you can use. more details here: https://github.com/facebook/react/issues/2461 – Byron Wong Jul 12 '17 at 16:46
  • 1
    @ByronWong Error boundaries only apply to errors that happen during rendering. ErrorBundary is not a substitute for `window.onerror` as it does not cover errors originating anywhere else. – Gustavo Maximo Aug 01 '21 at 16:14
  • @GustavoMaximo that's correct. ErrorBoundary will only catch synchronous errors during React rendering. The original answer of using BatchingStrategy API caught more errors, but it's no longer available in React 16+. – Byron Wong Aug 03 '21 at 18:27
16

Error boundaries are too limited and don't catch all errors.

In React 17, to catch all errors, like:

  • events from promises (event handlers on click),
  • as well as sync exceptions like undefined exception, etc

You need two global handlers:

// TypeScript

export function registerHandlers(store: Store) {
  
  window.addEventListener("error", (event) => {
    store.dispatch<any>(setErrorAction({ message: event.message }));
  });

  window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
    store.dispatch<any>(setErrorAction({ message: event.reason.message }));
  });
}

Invoke this after the Redux Store is created and as a result, all exceptions will be passed to Redux, so you can useSelector to get it and display or log somewhere (e.g. send to server for storage).

For better coverage on HTTP errors you can catch them on Axios Response Interceptor and push to store from there (you will get more information about the error). Just fiter it out on unhandledrejection (unhandled promise exception) or swallow in the interceptor, so it's not doubled.

andrew.fox
  • 7,435
  • 5
  • 52
  • 75
2

I had the same problem. I created an Office App where I neither had a debug console nor developer tools, so I couldn't found out where errors occured.

I created a single component (an es6-class) that catched all console messages, saved the message into a separate array and called the "real" console function.

log(message) {
    const msg = new Log(message);
    this.pushMessage(msg);
    this._target.log(message);
}

where Log is a simple wrapper with a message and a type and this._target is a reference on window.console. So I did the same with info, warn and error.

Additionally, I created a method handleThrownErrors(message, url, lineNumber) to catch exceptions.

window.onerror = this.handleThrownErrors.bind(this);

At least I created an instance of the class (i called it LogCollector) and appended it to the window.

window.logCollector = new LogCollector();

Now I created an react component that gets the logCollector instance (window.logCollector) as property. In regular intervals the react component checks the collected messages and display them on the screen.

componentDidMount() {
    this.setInterval(this._forceUpdate, 500);
},

_forceUpdate() {
    this.setState({update: !this.state.update});
}

this.setInterval() is an own function that simply calls window.setInterval().

And in render() method:

return (
    <div class="console">
        {this.props.logCollector.getMessages().map(this.createConsoleMessage)}
    </div>
);

NOTE: It is important to include the LogCollector before all other files.

NOTE: The above solution as a very simplified version. For example: You can improve it by adding custom (message-) listeners, or catching 404 Not found errors (for js-scripts and css-files).

Rubens Mariuzzo
  • 28,358
  • 27
  • 121
  • 148
marcel
  • 2,967
  • 1
  • 16
  • 25