On throw in React

calendar icon2021-10-05
user iconRadosław Miernik

Intro

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 if or 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 (useReducer and useState have lazy initializers, remember?), all class components lifecycle methods (including componentDidCatch), event handlers (on*), and asynchronously, in setTimeout, setInterval, or somewhere in a Promise chain.

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> component: at 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 componentDidCatch or 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 throw an Error instance, a string, a number, or some other object – it will be caught, as expected. We can even throw null or undefined, and it’ll still work.

But what if we throw a Promise?

Suspense!?

If you somehow managed to “accidentially” throw a Promise, React will warn you with an entirely different and surprising message:

Example suspended 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:

Caution:

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 async/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.

Finally, let’s imagine JavaScript would introduce algebraic effects. I’d love to go into details here, but that’s not the point. If you’d like to go the “hard way”, here’s an introduction. If you’d like to go the “easy” one instead, do read this article from Dan Abramov. Or just skip both and let me (vaguely!) explain.

The idea is simple – instead of await, yield, and 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 (await), a yield handler (for..of), a throw handler (catch), and finally, a suspense handler (<Suspense>). Yes, we could define our own – and as many as we’d like to.

I don’t think it’ll happen, though. There are already some advanced languages that support algebraic effects – for example, Eff and Koka. However, virtually no one “in the mainstream” has ever heard of them, let alone used them. I agree that these are important – that’s the bleeding edge of language design, and we need those to progress. I’d say we’d rather switch the entire language rather than extend JavaScript or Python with effects.

Closing thoughts

While working “within” a language or a framework, we have to fit in. It’s not bad per se, but it’ll always result in limitations. One time it’s the API, another it’s the syntax. Here we’re “constrained with JavaScript” and that’s all.

Whether this <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.

References:

  1. 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.

  2. Please, don’t throw a null or undefined. Yes, you technically can do that, but you definitely shouldn’t. It’s terrible to handle these. Luckily, TypeScript 4.4 introduced the --useUnknownInCatchVariables flag.

  3. Even with a new Babel plugin, TypeScript feature, or even an entire language transpiled to JavaScript, we’re still being constrained by the host language. It may be the case that we actually will, and no one will ever throw an actual Promise object, but the bundled code shipped to the browser will.

Author

Radosław Miernik

Software architect at Vazco. He also publishes other articles on his blog.

Suggested articles

On Ad Hoc Mentoring

Our thoughts on the development of JavaScript ecosystem

On Recruiting Full Stacks

Let’s work Together!

Michał Zacher

Michał Zacher

CEO at Vazco

Like what we do? Let’s talk about your project
and build something your users will love.