Let's deal with those pesky errors once and for all
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.
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!
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.
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.
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 ❤️
All rights reserved © 2024