> Result is for expected domain failures. Panics are for programmer errors and unrecoverable constraint violations.
The problem is that "unrecoverable constraint violations" happen a lot in practice when you're dealing with filesystems, networking...anything that isn't pure computation.
Suppose I have a function that calls other functions that themselves make 3 database queries, two HTTP requests, and reads/writes from a cache directory. It considers all of them (except perhaps the caching) unrecoverable in the context of that function. What should it do?
I see three reasonable options:
(1). return a simple error type saying "Networking failure", "IO Error", etc if any of those fail
(2). return a complex error type that exposes the internal details of all the different things it's doing and which one failed and why
(3). panic if any of them fail
I would argue that (1) is unfit for purpose as you have no idea what's actually going wrong.
And (3) is currently very heavily discouraged, though I think if I'm understanding your argument right it probably makes the most sense. However it leaves your top-level function in the awkward position of needing to make that panic part of its API contract, without the type system to help. It's also highly limiting because the caller now can't distinugish between programmer errors and possibly-transient environmental conditions like a service outage.
(2) is what I'd expect to see in practice right now, and that's what leads to these automatic stack traces, etc. But none of these feel like good options. Ideally I'd want something that is:
- Debuggable (like (2) and (3))
- Part of the type system (like (1) and (2))
- Still allows introspection by the caller (like (1) and (2))
- Doesn't require a ton of boilerplate at each level (like (3), and possibly (1))
No, I don't think you understood the GP's argument. Network and filesystem errors are not always "unrecoverable constraint violations". They're often just simple errors -- things that you should expect to happen, even -- and your (1), or, better, (2), are the most appropriate reactions to those.
"Unrecoverable constraint violations" occur, for example, when you've done a sanity check on some data structure and found that it's in a state that should be impossible, and so continuing from there is unsafe.
Even then, you may choose to handle them in a better way than simply aborting the program. For example, if I'm writing a HTTP service that is backed by a database, and I get a customer request that results in me finding that a column in the database is NULL when it shouldn't be, I'll probably just return a 500 error to the customer rather than panic!(). The assumption is that even though there's a problem with this particular data, that might be the result of an almost-never-hit edge case, and we can still serve other customer requests just fine.
Sure, a simple single-user command-line application may choose to panic!() if a critical data file can't be opened from the filesystem. Maybe that is an "unrecoverable constraint violation" sometimes. But I think there's a lot of nuance you're missing.
It's fun to think through examples like this. But, of course, we need to exercise caution because so much is dependent on the specific contexts of each individual project.
First, I will say that I probably misspoke (mistyped...?) by using the word "unrecoverable". At the end of the day, it's not even really about whether or not something is recoverable, but it's really just about whether the caller might "want" to be aware of it and how much detail the caller needs.
For your example, you end up writing,
> It's also highly limiting because the caller now can't distinugish between programmer errors and possibly-transient environmental conditions like a service outage.
That's the giveaway that the caller needs to know about service outages, specifically. So, you need to handle your HTTP requests and/or database queries in such a way that you can incorporate some of the failures into your function's error type.
But, you SHOULD NOT just implement `From` for converting all of your database library's errors into your function's error type. You have to actually inspect the error returned from the database and return an appropriate error. Specifically, if you're using a SQL db library, it might return an error if your query generating invalid SQL statements--that should be a panic because that's not a "service outage", that's a programmer bug in the implementation of the function. Likewise, an auth error is not the same as an outage. If the db library specifically returns an error that it can't make a connection, then that's the one you'd want to wrap in your error type in this example.
But, again, it all depends on exactly what kind of project we're working on. Your example of doing HTTP, and filesystem, and database queries reminds me of Firefox. Firefox obviously does HTTP stuff, and it uses the filesystem and a SQLite database for settings or configs or something... So, if we were talking about your example function in the context of writing a web browser, then a failed HTTP request is 100% normal and expected because the user's device might connect and disconnect from the internet at any time. So, HTTP failures should be represented in the function's signature. However, since the SQLite database is basically part of the application, itself, any errors when trying to query it are probably panic-worthy. Phrased differently: it's a working assumption of the application that the database is always accessible, so there's no reason to describe failure modes that aren't supposed to ever happen. If the database ever became inaccessible, the top-level main function should catch all panics, log something about them (maybe send off telemetry data, etc), and warn the user that an unexpected error occurred and either tell them to restart the app or just kill ourselves, etc.
Have you ever written a function that returned a `String`? Or a `Vec`? Well, those require memory allocations and they may fail and panic. But, I've never worked in a context where it made sense to try to catch those panics and change those function signatures into `Result<String, OOM>`. My applications choose to assume that enough memory will be available, and I've made the decision to allow the apps to crash and burn if that assumption ends up violated rather than add the large burden of carefully handling that possibility in every line of code in these projects. And, so far, that has been the right call because none of my Rust projects have ever OOM'd yet (and some have literally been running in production for multiple years), and there's really nothing I would want to specifically do if they did-- I'd either figure out how to reduce the memory requirements or increase the server's memory.
The problem is that "unrecoverable constraint violations" happen a lot in practice when you're dealing with filesystems, networking...anything that isn't pure computation.
Suppose I have a function that calls other functions that themselves make 3 database queries, two HTTP requests, and reads/writes from a cache directory. It considers all of them (except perhaps the caching) unrecoverable in the context of that function. What should it do?
I see three reasonable options:
(1). return a simple error type saying "Networking failure", "IO Error", etc if any of those fail
(2). return a complex error type that exposes the internal details of all the different things it's doing and which one failed and why
(3). panic if any of them fail
I would argue that (1) is unfit for purpose as you have no idea what's actually going wrong.
And (3) is currently very heavily discouraged, though I think if I'm understanding your argument right it probably makes the most sense. However it leaves your top-level function in the awkward position of needing to make that panic part of its API contract, without the type system to help. It's also highly limiting because the caller now can't distinugish between programmer errors and possibly-transient environmental conditions like a service outage.
(2) is what I'd expect to see in practice right now, and that's what leads to these automatic stack traces, etc. But none of these feel like good options. Ideally I'd want something that is:
- Debuggable (like (2) and (3))
- Part of the type system (like (1) and (2))
- Still allows introspection by the caller (like (1) and (2))
- Doesn't require a ton of boilerplate at each level (like (3), and possibly (1))
(edited for formatting)