djbalin
djbalin4w ago

When to throw an error?

Hi, we're thinking about how to handle errors in our app in a streamlined way. By "errors" I mean not only errors/exceptions that Convex throws but also unexpected occurrences business-logic wise. For example, we enrich a lot of our data by fetching related or referenced documents. Each ctx.db.get can return null, but this would indicate a business-logic error: a video should not have a channelId which points to a null document. In such a case, we would probably want to entirely dismiss the request and not serve partial data (we keep track of error logs etc. using Axiom). The most straightforward approach in the backend is to throw new Error in all such cases, but that burdens the client with handling all error cases What's an idiomatic or useful approach? We are a startup building a React Native app and are not sure where to place the balance of handling errors. Do we burden the front-end completely with this and keep the backend ncie and deterministic (in terms of return types), or should we couple the backend more closely with error handling?
8 Replies
Convex Bot
Convex Bot4w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
lee
lee4w ago
I can describe the philosophy we use at Convex, although of course you're free to make other choices. There are fundamentally two types of errors: internal server errors and user errors. Internal server errors indicate it's "the server's fault". This includes dangling references (like a video has a channel id pointing to a null document) or any other constraint violation where code was supposed to ensure it never happens. It includes transient errors like you would get from a race condition between a user request and a clerk webhook -- which is the server's fault because ideally the clerk webhook would be instant. Then there are user errors. These are based on bad user input, like if they try to modify a document that they don't have access to, or they try to create a video in an invalid channel. These errors are always actionable by the user, because it indicates they did something wrong -- maybe they need to log in or gain access to the document. Internal server errors are thrown as throw new Error. They either show a 500 in the clieng or show a toast and restart the connection, hoping that the server will fix itself. We patrol Sentry for these bugs and fix them. User errors get thrown as throw new ConvexError with an actionable message. The client catches the error and either shows it to the user, or automatically fixes it by redirecting to a safe page (like home or login).
djbalin
djbalinOP4w ago
Thanks for your thoughts @Lee. Handling user errors seems straightforward enough - we explicitly define one happy path and show some error message for all other paths. Regarding server errors, do you catch potential errors/place error boundaries for each query? I mean, even the simplest ctx.db.get(id) could result in a server error. So the server provides no guarantees to the frontend, i.e. "any and every call to the server could call an error, and its up to you (the client) to handle that"? Hmm seems like the problem I'm describing is "graceful vs ungraceful error handling". It's very important for us to ensure a smooth user experience, so I think the best approach for now is to lean towards graceful error handling and, in most internal server error cases, dispatch an error log that we monitor in Axiom and return null or similar to the client
lee
lee4w ago
for internal server errors i would throw the error (returning null is dangerous because mutations might commit) and catch it in a top-level error boundary for the whole app. but i understand if the react native idea of graceful handling makes that infeasible.
djbalin
djbalinOP4w ago
Ah yes, I actually ran into that problem earlier when trying to go for amazing grace. I considered that a middleway could be to only throw internal server errors for the most low-level logic (i.e. shared helper functions) and then gracefully handling these errors within mutations/queries to ensure transactionality. But that just felt like disabling Convex' transactionality and trying to build it myself again but much worse!!
RJ
RJ4w ago
I also like to think of these two types of errors that Lee describes as "expected" vs "unexpected" errors, and the difference between the two is basically whether or not you anticipate them happening in the course of normal program execution, or not. Those that you do, you should account for. Those that you don't, you should maybe handle in some generic way on the client, but otherwise shouldn't bother trying to account for—better just to fail fast and report it. db.get is an interesting example. If the ID you're looking up is provided by the client, I think it makes sense to take its type signature (A | null) seriously. But if you're passing along the ID of a document you just retrieved, within the same transaction, you should really treat it's return type as A. I think it would better if there were an API like Ents has for this (getX), which just throws if the document is not found, and which you can use in scenarios like this. To not expect an error (as for "unexpected" errors) means to not necessarily understand how or why they happened. And without knowing how or why they happened, you can't know how properly (gracefully) to recover from them. Trying is often a fool's errand! If you're worried about things like dangling foreign key references, I think the best thing you can do is upstream of any particular attempt to query across tables—by doing things like writing validation queries that run periodically as cron jobs and verify that no such dangling references exist. And/or creating helpers/safe interfaces for performing operations on your tables, which enforce these (and whichever other) invariants you'd like.
djbalin
djbalinOP4w ago
Thanks a lot @RJ for your thoughts! Expected vs. unexpected is a nice way to think about it as well. I'm definitely mostly concerned with unexpected errors here - in particular dangling references or other similar soft or business-wise constraints that for some reason are not true (and typically show up as null being returned from db.get in our enrichment workflows). We're definitely going to implement some sanitization cron job to clean up dirty data and dangling references! I was inspired by one of the Convex team's YouTube podcasts/discussions where @James Cowling mentioned how he's often gone to great lengths to ensure data correctness and to sanitize/amend incorrect data very early on. That kind of peace of mind seems sooo nice! I guess the only strong argument for defensive and graceful programming in our backend is the "nightmare" scenario of some poorly-caught and widespread error causing our app to be unusable for a large number of our users. In such a case it would maybe(?) be better to have gracefully preemptively handled it and just unexpectedly send null to those clients - at least our IDE will explicitly have told us while developing the frontend that the return type can be null! On the other hand, I feel like that ^ kind of thought process may be a symptom of timidity or low self-confidence. Thoughtfully and consciously handling errors at critical points in the front-end is for sure a huge reward, but the potential acute risk seems larger as well
RJ
RJ4w ago
IMO still the best thing to do is use an error boundary on your frontend to catch unexpected errors and fail fast when/where they occur—this is your catch-all unexpected error handler. The risk of not failing fast is that such errors infect the rest of your system—maybe you actually end up writing some records to the database with bad data, for example, before things finally break (or, worse, you write bad data and things never visibly break). If there are specific things you're scared of that you know could occur and that would cause a lot of problems, account for those—but then I would also say that those are therefore (by merit of you knowing about them) probably more in the category of "expected" errors than "unexpected" ones. That's my take, anyways. In my experience, unqualified defensive programming for every unknown is just a very deep rabbit hole to climb down, and rarely pays off

Did you find this page helpful?