Open oscartbeaumont opened 1 year ago
Thanks for the ping on this! I feel like there's a few different scenarios for rspc that suggest different approaches to error handling.
Most web servers sit at the boundary of your application and can't make too many assumptions about the clients they're interacting with. In these cases, typically for error handling you resort to a lowest common denominator of HTTP. Usually you'll set a status code like 500
or 429
; you might also attach additional context, whether it's unstructured (human-readable text as the response body) or structured (setting a header like Retry-After
, setting Content-type: application/json
and returning a complex response).
In these cases, I think having a concrete target error type like rspc::Error
with support for setting status codes, etc, makes a lot of sense. That said, as I mentioned in the other issue, I did struggle a bit with the ergonomics of converting errors, especially when using third-party libraries in a procedure. It's obviously always possible to wrap them in a custom error type and to implement From<...> for rspc::Error
, as you say -- but to me this is pretty clunky. It reminds me a bit of the boilerplate of checked exceptions in Java (albeit not their semantics).
In this scenario of sitting on the application boundary and using the lowest common denominator of HTTP, something like anyhow
makes some sense to me: ultimately an error in a procedure means the client is going to receive a (somewhat) opaque error with some context attached to it, and there's not a huge benefit to fully custom error types.
On the other hand, rspc concerns itself with both sides of a client-server interaction, so I think there's an opportunity to potentially allow the user to have something better than just "semi-opaque status codes with optional context." Maybe there's a mode of operation where errors are typegen'd and serialized, so that the client can discriminate between them and potentially recover or perform meaningfully different work based on the result?
I know tRPC doesn't work like this -- but that's because it's severely constrained in what it can do by JavaScript's (and TypeScript's) model of error handling, where anything in the stack can throw anything, even a non-Error
. For its part, React Query is generic on error types so the door is open there for rich errors there, at least.
I should probably provide an implementation for From<anyhow::Error> for rspc::Error
. I personally don't use anyhow
but it is very commonly used so I think this would be a welcome edition.
The ability to type export the error type would be a very cool feature and sounds pretty easy to do as long as the user can decide on a single error type for the router like this proposal would do (the TErr
generic). I thought about implementing something like this early on and kind of forgot about it. I feel like I recall someone doing typesafe errors with tRPC but I can't find any reference to it in the docs.
The main issue we would run into with this idea is around getting type exporting to work with error types. This would also suffer from the orphan rule if you used an external error type because you would need to impl specta::Type for anyhow::Error
or the like. Regardless of this issue, I would still definitely like to go forward with this because it will allow for some very cool use cases (such as form validation errors created by the backend like GQL allows).
Maybe impl From<rspc::Error> for MyErrorType
could deal with the conversion inside of it so we don't need to deal with the question marker operators incompatibility with generics.
I wonder if the router should allow overriding the currently hardcoded error type. This would work by setting a
TErr
generic on theRouter
when it is defined (similar to what you do with theTCtx
type currently).Rational
@lostfictions left a comment on Brendonovich/prisma-client-rust#60 about the orphan rule being a struggle when doing error handling in rspc and I have been thinking about solutions to that. In my personal use with rspc I have never run across this as a problem but it is a completely fair use case I never really considered. I need to document a workaround/solution for it because it can be confusing for beginners.
One solution would be allowing the user to define their own error type and have rspc work with it as included in this RFC.
In Spacedrive we kinda sidestep this problem by using internal errors types (built using
thiserror
) that then implementingFrom<CustomError> for rspc::Error
(shown here). I wonder if custom error types in rspc would allow for better ergonomics around this?Downsides
Packages (such as PCR) that implement the error conversion will stop working if the user changes to a custom error type. This does seem like a pretty reasonable behavior. I wonder if the user could also wrap the
rspc::Error
type in their custom error type and rely on aFrom
impl to make everything work?