This article is about sharing my thoughts for implementing a React application error handling infrastructure and augmenting it with the observability that MobX provides.
If you need a refresher on React Error handling you can check the prequel at
Yet another React Error Boundaries explanation
Peter Perlepes ใป Oct 13 '19 ใป 3 min read
Tackling error handling gracefully in modern frontend applications is not as easy as it sounds and can generate many questions that will indeed affect the whole experience of your product or service.
How easy then ?
As an example for some of our own applications in my current company we would need to answer most of the answers below when an error occurs:
- Do I even need to do something ?
- Do I need to keep showing the loading indicator and retry ?
- Does this error signals a whole application failure ?
- Should I prompt the user to refresh or try again later ?
- Do I log this as an alert on our error reporting service ? (we use Rollbar)
Of course all of the above depend on the error that occurred and also on how complex the application is.
Just a tip: Start small. If you don't have anything set up currently, just add a really simple screen for the user that faced an application failure. Apologise and give him a simple alternative, either that be calling support or wait until the issue is resolved. Don't leave him with a blank screen or even worse, a broken experience.
While trying to tackle this issue initially, we quickly got into the point that we would need to handle asynchronous operation failures. In our case network requests mostly for resource fetching.
I am sure most of web developers out there have used code like this (ok it could be fetch
or any other library but you get the idea). The event of a network request failure is something that most probably should be reflected in the UI, presenting the current state of the application. In the case of handleError
, even if you throw
the error, it goes unhandled through React and any set Error Boundary. Are they any good then ?
Error Boundaries are good
As shown in the prequel of this post, Error Boundaries have the single responsibility of handling synchronous lifecycle/render exceptions, but in our view they are so convenient. You get to add a Wrapper
that acts as an erroneous state replacement for any Component
. Like that was not enough, you get to have as many as you want, each being able to handle specific case or re-throw for the next parent Error Boundary.
How we approached the needs
So in my mind to reap the most benefits out of the native error handling mechanism we would need to have two things:
- A reactive way to handle asynchronous code faulty behaviours and exceptions
- A centralized module that can fit the business logic categorizing error occurrences and modify state as needed
What these initial thoughts triggered is reaching out to MobX and creating an ErrorStore
, nothing fancy as of yet but enough for a start.
By having this store at hand, we get a centralized place to add errors that we indeed want to handle more strategically. With just a simple observer
decorator/function we can get access to the errors
member and allow any Component to get notified of updates.
Now this centralized error handling structure can weird out many people but let me remind you that this will not be a replacement of any sort of error handling that you might already have in place, but an augmentation.
It may seem too explicit for some people and it might get confused with manual handling, but it is not. What I would like to see it as is more of an additional filtering layer that you either pass-through or handle.
The good parts
As we have shown previously, the asynchronous HTTP request Promise rejection, was left unhandled by an Error Boundary. To get a first shred of control in this I added this exception on the ErrorStore.
As simple as:
axios.get("https://resource.example.com")
.then( res => handleSuccess(res) )
.catch( err => errorStore.addError(err) )
Now the requirement was that in the case of a failed response from this resource request, we would set an application level error and show a reduced availability screen to the user as it currently worked with any technically synchronous React error. Now this seemed really straightforward to do.
const MyComponent = () => {
const { errorStore: { errors } } = useStores();
// useStores -> https://mobx-react.netlify.com/recipes-migration
if(errors.length){
throw new Error("It seems the request went wrong, let the Boundary handle it.");
}
return (
<MyWellPlacedContent />
)
}
As it is in technical terms, inside the React responsible code, when you throw, the error will get handled by the next wrapping Error Boundary.
Success! ๐ ๐ ๐
But this...
// ...
const { errorStore: { errors } } = useStores();
if(errors.length){
throw new Error("It seems the request went wrong, let the Boundary handle it.");
}
// ...
seems to me as reusable HOC-ish logic. Something like a seatbelt (?!)...
const withSeatbelt = (ComponentWithDangerousOperation) => inject("errorStore")(observer(({ ...otherProps }) => {
const { errorStore: { errors } } = this.props;
if(errors.length){
throw new Error("Your logic");
}else{
return <ComponentWithDangerousOperation {...otherProps} />
}
}));
The above HOC is naively listening to any error that happens and triggers the throw logic that would use the next in line Error Boundary. Instead of the errors.length
conditional, you could start becoming creative. What I thought of adding:
- A HOC function parameter that defined
when
should I throw. E.g.if(test(errors))
. Thistest
function can search the array for a specific error you want for the Component to throw. - For higher reusability you can opt for adding the Error Boundary Component as a parameter, or even define a class of HOCs with specific Error Boundaries.
Logging, monitoring, tracking tip
What we usually do in the Error Boundaries of the React applications we manage is enriching the componentDidCatch
lifecycle with the required cross-cutting logic that keeps us in control. Calling the required services to send the error to:
- Error tracking service (e.g. Rollbar)
- Logging warehouse
- Google Analytics
This can now be used alongside the addErrors action
to also monitor these special kinds of errors.
This structure shown above has allowed our team to think about the probable states of any Component that we are building and not the underlying error handling mechanisms it should cater for, either sync or async.
I hope that if you reached the end of this article you have managed to extract even one or more ideas for your next or ongoing React application error handling. I would also be eager to hear more opinions about this matter.
P.S. If you haven't tried out MobX for state management, believe me you might be missing out on some state management pain alleviation.
Image by Quang Nguyen vinh from Pixabay
Top comments (0)