can you catch all errors of a React.js app with a try/catch block?
Try CatchReactjsTry Catch Problem Overview
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?
Try Catch Solutions
Solution 1 - Try Catch
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>
Solution 2 - Try Catch
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;
};
}
Solution 3 - Try Catch
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/
Solution 4 - Try Catch
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.
Solution 5 - Try Catch
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).