Rust Errors Without Dependencies
21 points by vaguelytagged
21 points by vaguelytagged
Why are errors hard in Rust
Coming from the try catch paradigm the typical way I write critical API errors is to throw one and bubble it up in middlware.
Am I the only one who thinks Rust's lack of exceptions is a breath of fresh air, that error handling has been simplified due to Rust's design, and that this kind of "middleware" where you just "bubble it up" is exactly the bad design I want to avoid? What is a completely unrelated piece of code going to do with the error anyway, other than maybe logging it?
Am I the only one who thinks that exceptions are a breath of fresh air, that error handling has been simplified due to exceptions, and that this kind of 'middleware' where you just 'bubble it up' is exactly the good design I want to separate my app's concerns in a manageable way? What am I going to do with the exception at the point I throw it anyway? I'm throwing it because I don't know how to handle it.
I'm throwing it because I don't know how to handle it.
That is exactly the problem. If you, the author of the code, who knows exactly what is going on at the point where the error occurs, have no clue how you should handle the error, then anyone else has even more trouble dealing with it elsewhere.
If I call your function, I want to know the ways in which it can fail. You, the writer of that code, should document every single error and tell me how I should react, given a particular error result. If you just do not care and throw, I will never be able to write any kind of correct and reliable code on top of yours.
If I knew how to handle the error then why would I raise an exception?
I'm the author of my code, but my code runs on top of other code as well as the kernel and the device, so there is a plethora of error scenarios over which I have absolutely no control. Of course I am documenting the errors that my specific code can raise. But I can't be expected to documented every possible error that can bubble up through my code, that is an absurd expectation.
Except in languages where errors are explicitly encoded in the type system (e.g. the Result type in Rust), you will in fact know what errors are produced. It's in languages such as Java where foo() can possibly produce a hundred different error types and you have no clear way of knowing which (yes, I know Java technically has checked exceptions but these are basically unused. Also Java is just an example language).
An unfortunate problem you'll likely encounter in languages that use algebraic types for errors (e.g. Rust) is that it's very tempting to define a single giant Error type per project (example). This however is more of an issue with the people using the system rather than the system itself, i.e. you can avoid this by being a bit more thoughtful. This is a nice article about the subject.
This is a nice article about the subject
Thanks for that. I'll probably have to read that at some point.
I think the issue is that there are situations where you just don't want to be that thoughtful about error handling and want to get some stuff done without unwrapping everything. Also I can roughly describe what I think should happen with an error, but then there's the issue of lining up the type system to do that (which can be a big issue).
In that case you can convert the error to a string and return something like Result<TheOkThing, String>. In fact, I find that for CLI applications you'll often end up doing just that at the top of the call stack. There are also cases outside of CLI applications where String is perfectly reasonable, especially if the alternative is an opaque error you can't meaningfully match against (e.g. it's an enum but the various constructors are implementation details users shouldn't have to care about).
When dealing with error enums I find that a good question to ask is "Can a user meaningfully act upon constructor X?" (or something along those lines). If the answer is "no" or even "maybe", then the constructor should probably just be something like Generic(String). I think there are actually many parallels between algebraic error handling approaches seen in the wild and how logging sucks in general, probably for the same reasons (= doing the dumb thing is easy, but doing the right thing is hard).
Generic(String)
Which is the equivalent of throw new Error(string), so we are back at square one š¤·āāļø
Except you still have the benefit of the possibilities being explicit, so you can still match against specific constructors if necessary. So no, it's not the same.
And you can catch specific exceptions if necessary, it's just not enforced half-heartedly by the type system.
it gives you and no "blessed way" to accomplish this from the community
https://blessed.rs/crates#section-common-subsection-error-handling is pretty much that ... ;P
Security
If those crates are compromised, then everything is pretty much compromised - look at the contributors and the blast radius. If someone managed to pwn David Tolnay in a way that had downstream effect, they'd pretty much pwn the entire rust speaking world.
Adaptability Using the standard creates a universal way to handle errors that most rustaceans will be familiar with. Almost everyone uses the standard library, which itself it extremely well vetted and battle tested.
The same thing applies to thiserror / anyhow / eyre really don't you think?
Even blessed rs provides multiple libraries for errors that page lists eyre and thiserror as options. Most popular rust projects aren't aligned on a single method. Not saying it's a bad thing but it is interesting.
I agree it would be a bad day but it does increase risk. Google chose not to for their rust adoption in chrome.
In some sense yes, but there was a situation in parking lot had a bug in it, and that's one of the most popular libraries in the ecosystem. Again not saying libraries are all bad, many are quite good. Just trying to provide alternative options and share why I wanted to try to use the std.
There won't be a single solution, because there are at least two different needs:
That's why there's thiserror for libraries and anyhow for apps.
yeah, the way the rust ecosystem is headed, we're heading towards an NPM situation. I use Rust but I try to minimize dependencies. Sometimes if the feature I need from a library is small enough, after I'm done writing the core features of my program I'll go back and reconsider my dependencies and even write my own subset of the dependencies tailored for my project if feasible.
The ecosystem really should start minimizing the amount of dependencies unless absolutely necessary. There's a lot of microlibraries on crates.io and there's also a lot of libraries/programs that requires 200+ dependencies for anything you might want to do.
IMO itās still much better than npm. Projects like tokio do good work consistently trying to minimize the dependency footprint. Iād rather have cargo than not but it does concern me sometimes. I think in due time things will change and the ecosystem will end up how it should. Especially as std gets stabilized and Linux starts using it more.
an NPM situation
A large and diverse set of very useful libraries alongside lots of people's unfinished projects and bad ideas which, in themselves, do not diminish the utility of the more polished libraries?
That seems fine to me, personally.
huh? that's not what I'm talking about here I'm talking about the fact that since everything has so many dependencies (regardless of quality), it becomes just a web of co-dependencies, so pulling in a single dependencies pulls in a lot more and if one dependency gets compromised, a lot more do.
What you said here makes no sense.
Yeah. I don't love nodejs but NPM is a glorious good and I can see why the ecosystem is so popular.
It's good to have a demonstration that you can do this without dependencies, but the reason libraries like thiserror exist is so you don't have to write all this boilerplate for 50 different error types. Macros let you define things much more succinctly. Pros & cons to both approaches.
My main issue with Rust errors, and the reason I started to use anyhow recently, is that errors are not composable. If you have a function that calls two other functions that return a Result with different error types each one, you probably need to define a new type again for this function, which wraps both things. And then, you are no longer able to use the ergonomic ? operator directly, you must do a map_err before.
And I get it that sometimes you might want to do just that. But it seems like there should be a better way to compose errors. If Rust supported anonymous sum types, you could still get some ergonomic benefits. In Scala there's scala.util.Try which is very interesting as it's similar to Either but the Left side is always a Java exception. And because of that you can reduce a good amount of boilerplate.
The From trait is for composing errors. The ? operator adds .map_err(From::from) for you.
You need to write a new type and implement the From for it, which is a bunch of boilerplate, and the reason why everyone writes a library to automate it.
OCaml has anonymous sum types which work in exactly the way you might expect. But it's still more convenient to use exceptions.
The sum types are interesting, maybe someone knows if thereās a way to do a catch all impl? I wouldnāt really say itās lack of composability but more lack of an _ match type equivalent for impl from
You can write impl From<T> for MyError where T: std::error::Error and store the T as Box<dyn Error>.
Feel free to introduce distinct error types for each function you implement. I am still looking for Rust code that went overboard with distinct error types.
Iāve seen it, but I wonāt be specific because I donāt want to shame anyone š The way I see this happen is through a couple of different emergent behaviors.
One is that the āmainā error type for a library, crate, subsystem, etc is tree-shaped. The more functionality that sits under this entry point, the wider the tree is, and the more error variants you have. Introduce a special case here or there per subsystem and youāve now got something unwieldy. Thereās definitely a case to be made that if your error type is feeling the pain of the complexity underneath this interface, some of the subcomponents of the system need to be factored out and composed in a different way.
The other way I see this happen is essentially āerror variant per call site.ā In other words, for the interface that can return this error, the implementation returns a different error variant at each error-returning call site in its implementation. An example is āfile not foundā: you may see a different variant for each type of file not found, or for each context in which itās not found. Again, there are better ways to handle this, but it is easy to go overboard when youāre trying to do The Right Thing by being explicit about all of the different error cases.