I wasn't sure where to report this, but this is a problem I'm having with a new feature.
I just finished upgrading my team to React 16. Overall, I think the upgrade is great. We are using fragments and the relaxed constraints on what you can return from render(), and I'm overall very happy with it.
My only issue is with error boundaries. The feature seems to be well-intended, and does help out quite a bit. The improved error reporting especially is beautiful and will make debugging a dream.
The problem is that I believe we are leaving our users in a worse state by having to display fallback UIs. In a perfect world, we would have no client side errors and none of this would even be a discussion. But unfortunately we do have to deal with client-side errors. I agree that leaving the UI in an unpredictable state is less than desired, but I would argue that it's still better than taking away the UI completely. If a user triggers a client-side error, but the UI is still there, they can continue to use the site with little-to-no issue in the case of most errors. But with React 16's new functionality, even the smallest of errors will unmount the UI.
Our options are:
Either way, with this new functionality, we need to have at least a top-level error boundary to display something to the user. Our top-level boundary still displays our navigation bar so that the user can still navigate away from the page and use other parts of the site. The alternative is that they will need to refresh the page because everything will be unmounted.
I agree that displaying a fallback UI can be useful in instances where you expect an error in some cases (for example, when loading an image from a third party CDN or something). But in cases where a bug is producing an unexpected error, we should still be able to keep the UI in its previous state.
This is how I envision it working:
If you are handling an expected error, you should display a fallback UI. If you are handling an unexpected error, you should try to leave the UI in the most unbroken state possible for the user. The error should still be reported and caught by a top-level error boundary so that it can be logged, but the UI should not need to be replaced in all instances.
Perhaps I'm missing out on a standard process for handling these problems I'm talking about, and I am happy to hear what other people are doing to get around this, but I believe this feature as it exists today means more work for developers, and poor functionality for users.
I believe the blog post about error boundaries mentions our experience with leaving UI in a broken state. Yes, sometimes it is okay, but unfortunately there are cases where leaving it in place is really bad (sending message to a wrong person, displaying the wrong price etc). The thinking behind error boundaries was informed by these cases (that were not very common but were severe when discovered).
The feedback we received from deploying this behavior at scale at Facebook was that it was a little annoying at first, as different UI elements disappeared, but it uncovered many dormant bugs, and was a net win in the end.
Take significant developer time to implement fine-grained error boundaries that will still leave most of the UI untouched, only replacing components in error
I’m not sure about how significant this time investment would be, but definitely yes, we explicitly mention in the release notes that as you upgrade to React 16 you need to pick some components and place boundaries strategically around them. Please consider this as an important part of the update process and not just an afterthought. I would start with a top level boundary and then a single boundary for any “layout” content areas (e.g. sidebar, navbar, main content), or any components that you don’t have high confidence in.
As a quick and dirty solution you can create an error boundary that just renders null in the error state. This isn’t pretty as UI elements will just disappear nowhere but gives you a migration path if you can’t invest time into designing the error states intentionally.
I understand this might not be helpful. But this a bit like asking to add Visual Basic’s on error resume next to JavaScript. The only reason this seems sensible to us is because that’s the status quo for JS libraries (including React <16) And because there’s associated cost of the migration path. However, in our experience, once you get through these humps, the new behavior is actually quite nice and makes sense.
Also, it’s worth keeping in mind that boundaries are just React components. You can get creative with them. For example if keeping the UI in place is important, you can add a “Retry” button to the boundary that would reset the error state (and make it attempt to render its children again). If the error was transient (e.g. related to state that got reset), the user will see the component again. Otherwise it will render the error state again.
That's actually an interesting idea. I'll have to start thinking a little more outside the box for these.
My primary reasoning for the suggestion was that while we fine-tune exactly where we need error boundaries and what they should do, we are leaving a bad user experience because errors are no longer silent. This is why I was suggesting that the "fallback UI" concept be more of an "opt-in" feature. You can add error boundaries to start taking advantage of the error reporting/handling features now, while still behaving like React <16 by default (no unmounting), and as time goes on, you can start to add fallback UIs where you see that they are needed.
The feedback we received from deploying this behavior at scale at Facebook was that it was a little annoying at first, as different UI elements disappeared, but it uncovered many dormant bugs, and was a net win in the end.
This is actually lining up with my experience so far. While I was doing regression testing after the migration, I started running into errors from both before and after the migration, and it was much easier to debug them.
I understand that there is really no easy migration here, so we will try to think of creative ways to leave a good user experience while responding quickly to error reports when we see them. Once all is said and done, we will have a more robust app.
My primary reasoning for the suggestion was that while we fine-tune exactly where we need error boundaries and what they should do, we are leaving a bad user experience because errors are no longer silent.
Arguably UI that doesn’t update or does the wrong thing is also a bad user experience, just a less obviously visible one. We did the gradual migration by running the code with the new engine for a subset of users, and gradually ramping up the fraction, but that’s much harder to pull off with the final releases (since 15 and 16 have other differences). I’m sorry I can’t give you a better suggestion.
This is why I was suggesting that the "fallback UI" concept be more of an "opt-in" feature
I understand your proposal, and this is something we have considered ourselves early on. It has, however, turned out to be very hard to implement without introducing more fragility, and we have decided it was not worth it after the initial migration period.
Sounds good! Well we have some context and additional suggestions for how to proceed, so I'll close this.
I'm going to close this.
I know it can be inconvenient in some cases for existing apps which don't really have any UI error handling. But taking some time to implement error boundaries pays off and is better in longer term.
All proposed alternatives have sufficiently bad drawbacks.
Keeping "broken" and inconsistent UI on the screen can lead to wrong actions, like sending a message to a wrong person. That's very bad and erodes users’ trust in the UI.
Making “broken” components automatically disappear also leads to bad consequences. Like failing to show an item in a shopping cart, or hiding a privacy selector on the post input.
There’s a reason that when a function throws, JavaScript doesn’t just return null, or the previous function value. You have to handle the error, and choose the appropriate level of granularity. The same applies to UI.
I hope this makes sense, and I’m sorry for the trouble.
Most helpful comment
Also, it’s worth keeping in mind that boundaries are just React components. You can get creative with them. For example if keeping the UI in place is important, you can add a “Retry” button to the boundary that would reset the error state (and make it attempt to render its children again). If the error was transient (e.g. related to state that got reset), the user will see the component again. Otherwise it will render the error state again.