On throw in React
Library authors are doing their best to provide the cleanest APIs while being constrained by the language. The (clean) API itself is there not only to help people understand the code better without diving into implementation details but also to prevent them from foot guns of any sort.
Of course, the bigger the target audience is, the more vital the API becomes. If it’s not going to be “good enough”, then the community won’t hold itself, as new people won’t settle in quickly. Quality over performance!
What if we
abuse use some language features in an unexpected way to achieve an excellent developer experience? On the one hand, we achieved our goal; at least partially. On the other, it may result in interesting behavior. That’s how I see the future (of React).
Errors and exceptions
The role of exceptions in programming is to signal that something happened. It doesn’t have to be a reason to terminate the program immediately – they are often a result of parsing an incorrect user input or network failure. Of course, it’s better to parse than validate, but it’s not so popular approach… Yet.
There are also errors – traditionally unrecoverable – often coming from outside of our programs (OS, etc.). Probably, the most well-known is the stack overflow error; for the second one, I’d bet on out-of-memory error. However, it’s pretty common not to distinguish them anymore.
Over the years, exceptions (and errors; I’ll use both terms interchangeably) become a control flow structure, just like
while. I think that exceptions used to control the program flow are not a good idea at all. Is it an anti-pattern? Well, it depends.
Python goes even further and utilizes exceptions for such a basic operation as iteration. And yet, it’s not that big of a deal, as existing language constructs (
for) handle the
StopIteration exception implicitly.
An alternative approach, promoted by most (all?) of the functional languages and libraries, namely using
Result type, is easier to understand and to build tooling around. The most important point here is that it doesn’t require any extensions of the syntax or type-system.
Errors in React
In React, an error can occur in a bazillion of different places. It may happen synchronously in the render function, all hooks (
useState have lazy initializers, remember?), all class components lifecycle methods (including
componentDidCatch), event handlers (
on*), and asynchronously, in
setInterval, or somewhere in a
What if an error actually occurs? It doesn’t have to be thrown explicitly – it can be a bug or some unhandled case. In this case, you’ll see a message like this one:
The above error occurred in the
Example. Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
The error boundary is a standard React component, implementing either the
static getDerivedStateFromError. Simple as that, but handles only the synchronous errors – all of the others have to be handled “manually”, outside of React.
But is it actually true that it handles all errors? Yes and no. If we’d
Error instance, a string, a number, or some other object – it will be caught, as expected. We can even throw
undefined, and it’ll still work.
But what if we
If you somehow managed to “accidentially”
Promise, React will warn you with an entirely different and surprising message:
Examplesuspended while rendering, but no fallback UI was specified. Add a
<Suspense fallback=...>component higher in the tree to provide a loading indicator or placeholder to display.
Wait, what? Why React would use such an approach for… Anything!? The point is that it doesn’t even matter. What is more, they explicitly tell us not to focus on the API just yet:
This page was about experimental features that aren’t yet available in a stable release. It was aimed at early adopters and people who are curious.
Much of the information on this page is now outdated and exists only for archival purposes. Please refer to the React 18 Alpha announcement post for the up-to-date information.
Before React 18 is released, we will replace this page with stable documentation.
At the same time, Relay – Facebook’s framework to work with GraphQL – is already Suspense-ready. I agree, they had to test it to get a proper feedback from the community. The actual API is only an implementation detail – it’s going to be part of some nice, high-level API, suitable for everyday use; most probably accompanied by a hook.
Alternative API ideas
At this point, I think we can all agree that this
abuse clever usage of
throw is not ideal. Let’s think about the alternatives then. Our task is to have a sane API that can handle asynchronous operations in a synchronous manner.
Wait, that’s precisely what
await is for, right? Yes, but then the entire React stack has to be aware of
Promises. That’s not bad on its own, but it’d lead to a lot of complexity, as
Promise objects are not introspectable – all we can do is to
.then them and hope for a result. Also, there’s no way back – once we wrap anything in a
Promise, anything we’ll do with it will remain wrapped.
Another, very similar, would be to use generators. These are already better, as they don’t poison everything with asynchronicity. However, again, the entire stack would have to be aware of generators everywhere.
The idea is simple – instead of
throw, we’d like to have a single language feature (it has to be a part of the language!) that can handle all of them. It means it can “suspend” (!) the function execution but also resume it, maybe with some value. In terms of the three, it’d behave just like
yield, but it’d propagate like
throw – up to the nearest handler.
And just like with
throw and errors, we can handle the effects at any level. We could have an
async handler (
yield handler (
throw handler (
catch), and finally, a suspense handler (
<Suspense>). Yes, we could define our own – and as many as we’d like to.
<Suspense> API will stay or the React team will come up with a different approach doesn’t really matter. It’s just an API. We’ll use it. We’ll build tools around it. We’ll manage. We always did.
In Java, there’s a concept of checked exceptions. The idea is simple – every function (method) carries the information about all the possible exceptions it can throw. If you’d like to read more about them, definitely check out this Stack Overflow thread. There’s also this proposal, to add it to TypeScript. Nim has its exception tracking, which is basically the same idea but allows us to prove that a function won’t throw anything.
undefined. Yes, you technically can do that, but you definitely shouldn’t. It’s terrible to handle these. Luckily, TypeScript 4.4 introduced the
Promiseobject, but the bundled code shipped to the browser will.
Software architect at Vazco. He also publishes other articles on his blog.