Don't you just hate errors? They are the worst. They come in when you least expect it, and ruin a perfectly good user experience. They might put your app in a half-broken state, and there aren't any great ways to report errors in a consistent way.
Let's fix that.
Let me introduce error boundaries
With React 16, we got a new feature called "error boundaries". They are special components that implement a few special lifecycle methods, and that work like big declarative catch blocks. With a bit of trickery, we can even make them report ridiculously usable error messages to an API of your choosing! Interested? Let's dive in!
As I mentioned, error boundaries are just regular components that implement a
few special lifecycle hooks. The most important one is
static getDerivedStateFromError
. That method is called whenever a descendant
component throws an error while rendering.
Note! Error boundaries doesn't catch errors in event handlers, async code or while doing server side rendering. Just a heads up!
Show me an error boundary!
An error boundary is just a simple component, really. Here's a very basic implementation:
import React from 'react';
class ErrorBoundary extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
return { error };
}
render() {
if (this.state.error) {
return (
<>
<h2>An error occurred!</h2>
<p>
Sorry about that! We'll try to fix it as soon as possible. Until
then - try again!
<pre>{this.state.error}</pre>
</p>
</>
);
}
return this.props.children;
}
}
The getDerivedStateFromError
method receives an error, and returns the new
diff in state. Basically like calling this.setState(diffInState)
, and very
much the same like getDerivedStateFromProps
. Makes sense?
The component up top shows its children if something cracks under pressure, and an error message.
You'd typically wrap it around the outer part of your application like this:
const Root = props => (
<Provider store={someStore}>
<Router>
<ErrorBoundary>{props.children}</ErrorBoundary>
</Router>
</Provider>
);
Note though - you can place this error component wherever you want. Facebook has this great example with Messenger - where they don't want to crash the entire application just because the input field for new messages crashes. You'd still like to see the messages, right? Fact is; you can wrap whatever you feel like! You can start with a global one up top, if you want, but consider creating more specialized ones downstream.
Logging frontend error
Dealing with front-end errors used to be a huge pain in the butt - and for many it still is. Luckily - with React - you can intercept these errors and do whatever you want with them!
As an example - let's post them to an API. We'll use the other special error
lifecycle method, componentDidCatch
, to handle side effects like logging.
import React from 'react';
class ErrorBoundary extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
try {
fetch('/error', {
method: 'post',
body: JSON.stringify({ error, info })
});
} catch (e) {
/* fuck it */
}
}
render() {
if (this.state.error) {
return 'such error'; // as before
}
return this.props.children;
}
}
With the componentDidCatch
lifecycle method, we get two arguments - the error
itself (an "undefined is not a function", perhaps?), and an info
object, that
contains a componentStack
key - great for debugging errors in production.
Of course, you can use error reporting services like Sentry, or you can just create your own endpoint.
But, but... what if!
As mentioned earlier, some errors aren't caught by these error boundaries. How do we track those?
It turns out, the DOM has this
amazing window.onerror
error handler
provided for us. You should probably attach that same error handler to this one
as well, especially for error logging. So in your wrapping error boundary
component - add that error listener as well:
import React from 'react';
class ErrorBoundary extends React.Component {
state = {
error: null
};
static getDerivedStateFromError(error) {
return { error };
}
componentDidMount() {
window.onerror = this.logError;
}
componentDidCatch(...args) {
this.logError(args);
}
logError(args) {
try {
fetch('/error', {
method: 'post',
body: JSON.stringify(args)
});
} catch (e) {
/* fuck it */
}
}
render() {
if (this.state.error) {
return 'such error'; // as before
}
return this.props.children;
}
}
That's it! Error handling and debugging has never been this simple ❤️