specta-rs / rspc

A framework for building typesafe web backends in Rust
https://rspc.dev
MIT License
1.17k stars 55 forks source link

[RFC] Custom error type #109

Open oscartbeaumont opened 1 year ago

oscartbeaumont commented 1 year ago

I wonder if the router should allow overriding the currently hardcoded error type. This would work by setting a TErr generic on the Router when it is defined (similar to what you do with the TCtx 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 implementing From<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 a From impl to make everything work?

lostfictions commented 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.

oscartbeaumont commented 1 year ago

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

oscartbeaumont commented 1 year ago

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.