DEV Community

Cover image for Exceptions in Reactivity

Posted on • Originally published at

Exceptions in Reactivity

Very often programmers do not think about emergency situations. It’s especially sad when these are not application developers, but authors of libraries and frameworks.

For example, when I was preparing this material, I asked in the Effector chat how the system behaves when exceptions occur. To which they answered that there should be no exceptions in pure functions (exceptions, by the way, do not actually contradict purity, but that’s another story) and if you allowed them, then you yourself are a fool. When I asked whether the author even knew how his library would behave in an emergency situation, I was accused of being toxic and banned.

Well, we digress. There shouldn't be bugs either, like other bad things in life, however, they sometimes happen. The fault of the programmer, the browser, its extensions, the operating system, the stellar wind - it doesn’t matter. You have to be able to take a punch and not bury your head in the sand. But in different libraries we will encounter no matter what kind of behavior...

🎲 Unstable: Unstable operation
⛔ Stop: Stop work
🦺 Store: Error indication and waiting for recovery
⏮ Revert: Revert to a stable state

🎲 Unstable

Often, in the event of an exception, the application goes into an inconsistent state, resulting in unstable operation.

In the example, let's say an incorrect codepoint has crept into the name. And, let's say, an attempt to take the length of a string leads in this case to an exception. The example is quite synthetic, later I will show more realistic ones, but for now that’s it.

And so, when calculating the invariant, an exception occurred, which is why runtime did not update Count. As a result, all states split into 2 subgraphs, which themselves are consistent, but are no longer consistent with each other.

⛔ Stop

An equally strange solution is to simply stop functioning, as, for example, RxJS does. If an exception occurs anywhere in the stream, then all streams after it are finalized and will never work again.

This strategy is suitable for a single task - you either do it or fail with an error. But reactivity is for long-lived systems that constantly display something. This means that if reactivity fails, the application will simply break without signaling this to the user.

Moreover, it will only break by half. And to restore operation, you will need to restart either the entire application, or at least this half.

An incident from life: A crowd of athletes moved in with me on the hotel floor. And these are guys with 100 kg of pure meat. And so, we crammed into the elevator with them this morning, which predictably led to overload. The elevator raised its paws and said "that's it."

Well, okay, the couple left - nothing happens. Ok, another half came out - still nothing. So, everyone got out - the elevator still didn’t work. And we all had to do a run up the stairs today. I think the software for this elevator was written in RxJS, no less.

⏮ Revert

Libraries like Reatom, in principle, do not allow inconsistency by recalculating invariants within a transaction. So if something happens, all states are rolled back to the last consistent one.

Formally it doesn't sound bad. But for the user this is terrible behavior, because of one black sheep somewhere in the corner of the application, which constantly throws exceptions, our entire application comes to a standstill and does not react in any way to the user’s actions. Or simply - it hangs tightly. Which is no good.

🦺 Store

It is much more practical to consider the error as a possible result of the calculation, along with the return value.

Here, all states that depend on the incorrect one are also marked as incorrect. And the rendering system can automatically show a failure indicator for parts of the application that fail to update. Well, or you can catch the exception and draw your own beautiful message. In any case, the user will understand what is happening and that the faulty part of the application should not be relied upon. But you can continue to use other parts.

Even though part of the application is broken, the state of the application is still consistent. Because the error message at the output is precisely consistent with the incorrect value at the input.

At the same time, eliminating the cause of the failure will automatically restore the correct operation of this part of the application. Without additional movements on the part of the programmer!

Exceptions in $mol_wire

The reactive state can take one of 3 possible value types:

  • Result - the actual valid value.
  • Error - information about an exception.
  • Promise - a promise that either a value will appear soon (Result), or an explanation of why it does not exist (Error).

These values can arrive in the following ways:

  • return - the value returned by the function.
  • throw - interrupt execution.
  • put - directly writing the value to the cache.

In this case, it doesn’t really matter which way we delivered the value - it is written to the cache. When accessing state, the behavior depends only on the type of the value:

  • Errors and promises are thrown via throw.
  • The remaining values are returned via return.

That is, the reactive memoization of methods is not completely transparent: if you return an instance of Error, it will still be thrown later, and if you throw, for example, a string, then it will still be returned later. It turns out to be a kind of normalization of behavior.

We could, of course, make transparent behavior, but in JS we already have normalization with asynchronous functions: no matter what we return, they always return a promise. And promises, by the way, in our case need not be returned anyway, but thrown. And more to come later..

Top comments (0)