-
Notifications
You must be signed in to change notification settings - Fork 2
Make Reducer generic over errors #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Make Reducer generic over errors #4
Conversation
|
@DivineDominion Ahh… interesting! I'll put a little thought into this… it might be a good idea to have this ready for a FWIW… the very early versions of |
|
@DivineDominion BTW do you know what the runtime requirements would be for a typed throws? I know this requires the 6.0 toolchain compiler… but once that code is compiled are there any runtime requirements on how far back that code deploys to? |
No, I have no clue, or how to find that out except ask on the forums :) |
An alternative to a third parameter is for the Error type (which might be Conceptually… I could see the argument that an "error" does conceptually "belong" to a "state". I just don't know if I want to introduce a new protocol here. Hmm… |
|
An orthogonal observation here is that one of the important design philosophies of the whole If we do make the decision to formally define the type of error being thrown… we then potentially begin to "couple" our view component implementations to details made in the State layer. If the engineer building the Reducer decides that the type of error thrown needs to be changed… this is now a potential breaking change in our view component trees. If the engineer building the Reducer transitions from throwing no errors now needs to throw… this is now a potential breaking change in our view component trees. I could see product engineers defining a Reducer with 10 action values. None of those action values lead to an error being thrown. The product engineer builds 20 or 30 view components assuming that no error is being thrown from |
|
Thinking through things from another POV… I can count two major side effects from shipping typed throws on Dispatchers:
If you can think of number 3 and beyond please also bring that to the discussion. So let's think through number 1: cleaning up view component code when Dispatchers never throw. Once product engineers build complex products with 10s and 100s and even 1000s of action values I think that most real world products would throw. If "just one" action value can throw an error… that means the reducer itself is now throwing. For the Counter sample app… yes there is some boilerplate and ceremony from the And then this also leads back to a previous point: if a product engineer builds their component trees around a reducer that never throws and then adds one action that might throw… this is now a breaking change across those component trees that now need to implement a Thinking through number 2: what do we think the tradeoffs are of view components knowing exactly what error is thrown without having to unwrap an existential This also leads back to a previous point: we can now potentially couple our view component implementation to logic built in our Reducer. This can make view component code brittle and more vulnerable to breaking changes if the engineer working on Reducer needs to refactor the underlying Error. If we did implement typed throws there is nothing stopping a product engineer from choosing the type to be On a different topic I think something to keep in mind for both solutions is that we would like to think about ways to keep view component code loosely coupled to internal details from our State layer. Usually that means that properties of State are not And then another topic here worth keeping in mind is that from the perspective of view components we are asking ourselves whether or not a dispatcher can throw… which is not necessarily the same as asking ourselves whether or not a reducer can throw. Our current Tying this back to the two side effects from the start:
I expect the majority of product engineers using ImmutableData build Reducers that throw from at least one action value. I also believe that if we one day decide to give
I do agree there would be a runtime performance improvement from statically dispatching an error without an existential box to erase the type. But if we did statically dispatch an error I believe I would see a lot of benefits from keeping that error opaque. The actual data and properties of that error should not need to be exposed as Thinking through both of those… I'm leaning in the direction of not shipping typed throws on dispatchers for |
|
cc @octovolt for philosophy and history of error handling from flux and redux. |
|
I did a little more exploring for some history behind error handling in Flux and Redux. It looks like Flux did allow for Dispatchers to throw errors: And Redux also allows for Dispatchers to throw errors: https://redux.js.org/api/store#dispatchaction Because Reducers can sometimes do work they are not supposed to be doing it looks like Redux Dispatchers are setup to accomdate Reducers that throw: https://github.com/reduxjs/redux/blob/v5.0.1/src/createStore.ts#L298-L303 This implies that Reducers can sometimes perform work that might throw. What is less clear to me is if it was ever a legit pattern for product engineers building their JS reducers to throw from inside their reducers. I found arguments on both sides of that question… |
I wanted to discuss what you think about this, but figured it's easier with actual code in hand, so here's a PR -- not necessarily meant to merge as-is, but as a starting point.
So with SE-0413 Typed throws we get an alternative to the (cumbersome)
@rethrowsprotocol annotation: set the associated error toNeverto express that the function cannot throw.My motivation to look at this was that even the simple Counter example had to handle a throwing store dispatcher -- even though there were no errors.
With modern Swift and typed throws, we can essentially tell the compiler that this is not "throwing any error, or maybe none at all", but be very precise.
At the cost of a 3rd generic type parameter.
Counter: non-throwing
I started using this after making the reducer generic in d661685 by implementing a test suite to see whether it compiles and does its job:
This led me to tweak CounterUI and friends instead that do a similar job at exercising the API.
In a4c7f1a you see that one preview is now commented-out because you cannot decide that sometimes you want errors, sometimes you don't.
In real-world scenarios, that should (could?) be a feature. But here we see the first limitation of the approach. The
StoreandDispatcherneed to declare that errors are expected.This reflects my motivation and I would call this scenario a 'win'.
Animals
AnimalsReducerdoes in fact throw an error sometimes. The error needs to be public. You decided to put it inside the reducer, so it'sAnimalsReducer.Error.There's no typealias for the concrete dispatcher, so a lot of diff noise where:
Same for the
Store.The store double needs to declare
throws(AnimalsReducer.Error)even though it doesn't for compatibility as a stand-in. This may be a benefit (clarity) or downside 🤷Quakes
Same story to Animals, but dependencies fetched faster :)