-
Notifications
You must be signed in to change notification settings - Fork 147
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
If a function wrapped in action throws some error it should be possible to catch it. #387
Comments
Yeah I've been staring at this issue the past couple days and having some difficulties coming up with a solution I like so I want open up the discussion here on what I'm thinking. This starts with a few assumptions that I want to make about the solution.
Starting with these assumptions it is difficult to have a single behavior that satisfies everyone. For instance throwing an error on the submit function means that one can't rely on a Having a The one commonality I see in all this is we almost never want to actually throw known errors from user code. Because either it won't propagate and we won't get valuable information.. say like if it is a validation issue you might want to communicate that, or if we do communicate the error over HTTP then throwing afterwards is arbitrary and so baking that in doesn't seem to make a ton of sense. Throw is actually awkward in the latter because it removes our knowledge of what are legitimate responses we are dealing with (from TS or otherwise). I'd love to have more discussion on this topic and what people would want to see. |
I have some thoughts about the points you've mentioned.
There're 2 options to call actions: with // smth like this
type Submission = {
status: "pending";
} | {
status: "error";
error: unknown;
} pass action directly to the TBH I don't like this approach, because it separates the same action (error handling) with different syntax. So we need a unified mechanism which can be applied in any scenario. What we need to do? Handle errors for actions, so it means, this should be applied to the action itself (and not to Maybe we can go with the following: // make an action outside of a component
const myAction = action(() => {...});
// in a component:
// here it's possible to add several event listeners, because we can use the same action in different places and handle errors differently.
// in this case we also need to support `off` method.
myAction.on('error', (error: unknown) => {...});
// and only then you can use it with `useAction` With this approach we don't need to think about |
About Yes, |
Interesting, if a server action fails (for instance, there was an error when trying to write to a DB), why shouldn't it error? |
Anyways, it's not a simple question though. If an action never fails, eventually we need some way to handle errors somehow. Another take here: the complexity partially comes from 2 different ways of calling actions: one is passing an action directly to the form and another one is export function useAction<T extends Array<any>, U>(action: Action<T, U>) {
const router = useRouter();
return (...args: Parameters<Action<T, U>>) => action.apply(router, args);
}
what if instead of |
I probably have some bias here. I started in a system that didn't throw on status, then changed to it, and then changed back (superagent). Honestly I am not a fan at all on throwing on status and agree with the fetch spec here pretty wholeheartedly. Assuming we have the option to throw from a reasonable context.. we still have a problem if you want to This is what makes me think throwing to ErrorBoundary in general isn't really a go to here. Either you need a declarative API like So perhaps we should just not handle thrown errors at all. And server functions should fail on them as well. API stays the same we just throw when there is an error that isn't a response and that's on you. I guess that leaves forms in the dust, but not sure if there is much more to do for them. We could throw them from the router .. I guess.. but even then it isn't really great. What you'd want is to pass them back in through the declarative result. And now I'm back at the beginning again. |
I looked at other solutions. Remix throws from the route section, but their actions are tied to the route section so that works. They don't throw the whole page. Next uses useFormStatus to declaratively put the errors back in. I think what we need to do here is define something to represent a throw. So that the client can throw again. However I don't think in either case EDIT: I was wrong about next. I don't think they declaratively put errors back in. They expect you to use to return Errors just like Remix and Solid. And they do throw from the boundary if it isn't handled. |
Interesting.
Why is that? |
Well we could throw but the recommended way of handling wouldn't be the |
Maybe, but we're talking about the actions, right? In my opinion Maybe I'm missing some concepts about your definition of actions. |
If it is a form you need to be able to handle it so there is no place to have a We could force I just don't see how we could rely on the catch when you consider forms. Either we will be throwing up to the ErrorBoundary or we will be pushing it back declaratively. That being said every manual action looks like Which brings us back to whether these should be handled differently. Assuming we want manual actions to throw. I'm having a harder time deciding what is appropriate for forms. Especially if their behavior is to be so different. EDIT: This is one option. We always set it declaratively so you can read it off the submission. However, if it is a form we don't throw. I can tell when it is a form submission so we can just avoid throwing in that case. If you do the submission manually well it will throw. You can ignore it but that's on you, it will still be available through the submission as well. The biggest downside of this is people who don't look for the error with forms will never know. Still throwing to the ErrorBoundary is almost what I'd never want. Also developers would need to check both fields, because it is always better to return errors than throw them. Returned errors can be typed. I almost feel like Remix and Next throw errors to the boundary just because it is a pain enough that most people will return them instead. Yeah this still doesn't feel quite right. |
But it's not true...
It possible to have the same thing for errors:
The API of course can be different, but you got the point. EDIT: |
Another a little bit confusing thing (for me of course) is that we have two actions: manual and declarative. I mean, maybe, it could be done like: const myAction = action(() => {...});
<form action={myAction}>...</form> // declarative style
<form action={myAction.with({
args: [...someArguments],
catch: (error) => {...},
// extra options which can come in the future
})}>...</form>
myAction.bindToRouter().with({...}) // in a component before calling it
myAction() // manual call |
this |
Hm, another idea came to my mind. If an action won't throw an error, maybe not make it async? It will receive an object with callbacks (as XMLHTTPRequest for instance). Then, for people it will be clear where to get the result and where to handle errors (and any extra stuff). EDIT: And for convenience reason it could have |
I should have said form is also meant to work with no js present, so like with works because it updates the url. Any sort of catch/onError doesn't make as much sense. It needs to feed back in declaratively or represent via ErrorBoundary. |
The thing is if I don't like ErrorBoundaries then the only option is a split between declarative and throwing. ErrorBoundaries are the only solution that could cover both but they are my least favorite option. Thinking about what I proposed before the only thing I didn't like was having a separate error field. It feels odd. I wonder if we could get away with simply an error boolean. Then result just stays the same. Awkward for TS I suppose but it would still work the way it works now for forms except you'd know when it threw to get there and can identify the difference between known and unknown errors. I think TS will be a blocker though. I just hate the experience of having to always check for .error just in case. It is really nice how Remix and Next basically through API's discourage throwing unless it is an unredeemable error. |
Yeah I just went with my original proposal. |
Seems quite reasonable, I need to play with it, but I think it's ok solution right now. It's better to have a way to handle errors somehow than to not have it at all. |
Yes, it works. Closing the issue then. |
A side note here on Ryan talking about returning errors in the response just as you would return data. The works ok, until you start adding anything between the user and SolidJS such as a form validation library that needs to return its own errors in some way that you are not aware of. You would have to type the result in a way that is |
Describe the bug
Let's consider this example:
This error relates to this line: https://github.com/solidjs/solid-router/blob/main/src/data/action.ts#L97
Perhaps, handler should check the argument type (to preserve the current possibility to throw redirect or revalidate) and throw an error if the argument is not solid-related thing.
Your Example Website or App
see description
Steps to Reproduce the Bug or Issue
run this code
The text was updated successfully, but these errors were encountered: