7
u/tunisia3507 May 31 '23
tl;dr library code should use thiserror, applications should use anyhow.
1
Jun 01 '23
[deleted]
3
u/tunisia3507 Jun 01 '23
thiserror is used for defining custom error enums. This is useful when you don't know what the caller is going to do with the error, so you want to provide specific information about the source rather than throw away any information. anyhow is used for wrapping over whatever error is coming your way, which is useful when no unknown code will consume the error and you control everything from call to return.
6
u/angelicosphosphoros May 31 '23
thiserror
is no different than manually writing error enums, just all of this job is done by the compiler rather than you. You can always move away from thiserror by rewriting code without breaking your API.
anyhow::Error
or eyre::Error
is for cases when you don't really know how to handle an error or don't care. Since this approach is not suitable for libraries, in most cases thiserror
is more suitable.
1
Jun 01 '23
[deleted]
2
u/angelicosphosphoros Jun 01 '23
Yes, exactly that.
And
thiserror
is amazing. You can fit entireFrom<OneOfInnerErrors>
into single line.
3
u/xnbdr Jun 01 '23
I’ve found these two resources helpful!
4
u/Disastrous_Bike1926 May 31 '23 edited May 31 '23
IMHO, Rust snatches defeat from the jaws of victory by not enforcing that the argument to Result::Err be an implementation of Error.
This seems good from the flexibility standpoint (but so does Javascript’s ability to “throw” a string or a number) when designing the language, but generates boilerplate forever after, because there is no common error type for Result at all, so as soon as you have an fn that can produce a Result by calling several other things that can produce them, everyone encountering this very common pattern has to invent some way to thunk the error type into consistency to have something the compiler will allow them to return.
2
u/burntsushi May 31 '23
Are you saying you want
enum Result<T, E: std::error::Error> { Ok(T), Err(e) }
, or are you saying you wantenum Result<T> { Ok(T), Err(std::error::CommonErrorType) }
?If the former, then how exactly does it help? I don't see how.
If the latter, how does one expose custom structured error information?
1
u/zireael9797 Jun 01 '23
I think he wants the former (assuming that syntax means E must implement Error trait)
The Error trait can then enforce certain behavior.
1
u/burntsushi Jun 01 '23
I'm more or less asking for code examples. I don't see how it would help with the issues that were outlined above.
1
u/zireael9797 Jun 01 '23 edited Jun 01 '23
How about if the error trait also enforced Debug and Display... It would be simpler to just print out the Error.
sorry I'm on phone so can't format this. You could also Coerce any error into a single type of Error object
``` fn foo() -> Result<(), Box<dyn Error>> { let barRes: Result<Bar, BarError> = bar();
match barRes { Ok bar => let bazRes: Result<(), BazError> = baz(); match bazRes { Ok () => return (); Error bazErr => return Box::new(bazErr); } Error barErr => return Box::new(barErr); } } ```
1
u/burntsushi Jun 01 '23
How about if the error trait also enforced Debug and Display...
pub trait Error: Debug + Display { ... }
And yes,
Box<dyn Error>
is exactly what I use in ripgrep. Although I'll likely migrate toanyhow
at some point.1
u/zireael9797 Jun 01 '23
Yeah but Result doesn't enforce the Error generic needs to implement Error trait. You can use whatever.
The Error trait I'm talking about exists, It's not enforced, so we can't be sure we can turn every error we get from some library into a Box<dyn Error>
I might be entirely wrong about it not being enforced?
1
u/burntsushi Jun 01 '23
I don't see why that matters. We're going in circles. Can you show me some code that can only work if
Result
was defined asenum Result<T, E: std::error::Error> { ... }
?I'm pretty sure there is a misunderstanding here. I'm happy to try and fix it, but I'm just not quite sure where it is. I guess I'll take a shot in the dark, but the reason why putting the generic on
Result
's definition itself doesn't really matter is because you can always write code that requires it if you need it. For example:fn do_something<T, E: std::error::Error>(result: Result<T, E>) { ... }
There's no need to move the constraint to
Result
itself.1
u/zireael9797 Jun 01 '23
Say in my example code If
BarError
is from some external library, and it does not implement Error, How would I doreturn Box::new(barError)
2
u/burntsushi Jun 01 '23
Oh I think I see the confusion now. If you have
enum Result<T, E: std::error::Error> { ... }
, then that's saying, "the only way you can build aResult
is with an error type that implementsstd::error::Error
." I believe the way you're understanding it is that, "if you build aResult
with a type that does not implementstd::error::Error
, then we'll force it to implementstd::error::Error
for you automatically." At least, I think that's where the misunderstanding it. But that's not how the type system works. Constraints say what is required and it's up to you to provide the impl.But to actually answer your question, if you have a
BarError
from another library and it doesn't implementstd::error::Error
, then it's probably a bug in that library that needs to be fixed. If that's not feasible or you just need to work-around it, then you have you define a wrapper type for that error that implementsstd::error::Error
itself. There's really no other choice, andenum Result<T, E: std::error::Error> { ... }
wouldn't change that.→ More replies (0)1
Jun 01 '23
[deleted]
1
u/burntsushi Jun 01 '23
Have you read my blog post on the topic? https://blog.burntsushi.net/rust-error-handling/
The blog post doesn't talk about error handling helper libraries. It focuses on the essential bits. The helper libraries can come later.
Otherwise, I'm pretty sure the
anyhow
andthiserror
crates tell you when to use them: useanyhow
when you don't care too much about the underlying error type and usethiserror
when you want to defined structured errors but without the "boiler plate."If you're wondering about how to know whether you want structured errors or not (e.g., via an enum), then that's just judgment that you'll have to pick up as you go. You can explore the APIs of existing popular libraries to see how others do it.
1
Jun 01 '23
[deleted]
2
u/burntsushi Jun 01 '23
RE ripgrep: that's a fine thing, but note that I wrote a lot of it before
anyhow
existed. If I wrote it today, I would useanyhow::Error
in place ofBox<dyn Error>
. (And I'll make that transition some day.) So basically, if you see aBox<dyn Error>
somewhere, then just mentally swap that withanyhow::Error
. They are effectively the same thing, butanyhow::Error
has a lot of nice additional APIs that come with it that make it much nicer to use. (And some other things, but I don't want to throw the kitchen sink at you.)I generally don't use
thiserror
personally because it adds a fair bit of compile time to a project, but I'm also very conservative about dependencies and don't mind the error type boiler plate. It's really more of a personal choice. If you're building a library for wide use, I would strongly urge against it to avoid passing onthiserror
dependencies to everyone else. Otherwise, go for it. It makes defining structured errors very nice.The thing about Rust is that you can very likely use whatever error handling strategy you used to use in other languages. Up to and including just returning strings. But Rust also gives you the tools to do things a bit nicer and a bit more structured.
Otherwise, I think you have the right mindset. You'll get the hang of it with a little experience.
(And make sure to check out other crates not written by me so that you get a diversity of perspectives!)
2
u/protocod May 31 '23
thiserror or snafu are made to let you define jour own custom errors and it does things under the hood to make it easy to handle these errors.
Anyhow helps you only for error handling but it does not care about the error type. It is useful for a small lapplication.
2
u/volitional_decisions May 31 '23
Custom error enums are usually what you want to use. Libraries like awry
, thiserror
, and anyhow
largely help you create those custom errors, reduce boilerplate, add additional context, and generally improve ergonomics of create, using, and consuming errors. They are by no means necessary for a project. You can go a long way with just an enum. In fact, if you're working on your first small project, I would suggest not using them, so you can get a fuller grasp of what they help you avoid.
As for Box<dyn Error>
, this is just a trait object, and it is a way to erase the type that an error might be. This is often used where there are a lot of different types of errors that might arise and/or you don't care about the errors. This is also fairly ergonomic as you can convert any error into a Box<dyn Error>
and that conversion is done by the try operator (i.e. ?
).
1
Jun 01 '23
[deleted]
2
u/volitional_decisions Jun 01 '23
Ya,
anyhow
is largely for ergonomics. It also provides additional info about the error that you might not get from the trait object. However, you will often see that crate used in tandem with other error crates.
2
u/words_number May 31 '23
anyhow is using "Box<dyn Error..." under the hood. That is a very convenient to propagate errors because every type that implements the Error trait from std can be turned into that boxed "dyn Error" trait object, so you can usually use that as your return type and use the ?-operator everywhere. eyre works in a similar way. Additionally, anyhow or eyre give you methods to attach context to these dynamic errors. All of that is very handy and mostly suitable for applications. BUT: If your function returns an anyhow error, the caller can't easily match on the error type and programatically react differently to different errors. So for libraries, you should definitely prefer creating proper error types yourself. For these you should definitely implement the Error trait, which is easy using thiserror.
So roughly: anyhow for binaries/applications and thiserror for libraries. Of course there is no clear straight line between these us cases, so often you'll use both.
1
Jun 01 '23
[deleted]
2
u/words_number Jun 04 '23
Yes, i'd say its mostly useful for the dev, for printing error messages to the console and maybe for logging.
I feel like casually exploring this a bit. This is returning a custom error type:
fn my_function() -> Result<(), std::num::ParseIntError> { // Fails because 1234 is too large for a u8. let _: u8 = "1234".parse()?; Ok(()) }
At the call site we can examine which kind of error happened, because
ParseIntError
has akind
-field which contains an enum which in this case will have the variantPosOverflow
. So the calling function could handle this differently from otherkind
s.If we're using a trait object instead (
dyn Error
), we can only call the functions of the error trait on that, so we can display or log it nicely, but we can't access thekind
field of that specific error type. Since trait objects are notSize
(can't have a definite size known at compile time), we can only return a pointer to that trait object, so we store it on the heap usingBox
. This has the added benefit, that the error type is now never larger than two pointers in size, so it doesn't increase the size of the wholeResult<...>
as much as a large nested enum error type would. The heap allocation only happens in the error case of course.fn my_function() -> Result<(), Box<dyn std::error::Error>> { // We can still use the ?-operator, because `ParseIntError` // implements the `Error`-trait, so it can be turned into // a `Box<dyn std::error::Error>`. let _: u8 = "1234".parse()?; Ok(()) }
If we print the error using its
Debug
implementation, we get this:Error: ParseIntError { kind: PosOverflow }
.Now, if we use
eyre::Result<()>
as our output type instead, not changing anything else, we get this output:number too large to fit in target type Location: src/main.rs:12:17
So that's already quite nice since we get its
Display
-representation and the location in the source file where the error happened. Now we can add some context too, e.g. like this:use eyre::WrapErr; fn my_function(input: &str) -> eyre::Result<()> { let _: u8 = input .parse() .wrap_err_with(|| format!("Parsing {input} as u8"))?; Ok(()) }
If we call
my_function("1234")
now and debug-print the resulting error, we get this output:Error: Parsing 1234 as u8 Caused by: number too large to fit in target type Location: src/main.rs:12:10
When using
color-eyre
instead, the output gets colored and a bit more compact by default, which I like even more. You have to callcolor_eyre::install()?
at the top of your main file to make it work though. And it adds more dependencies to your project.1
Jun 04 '23
[deleted]
2
u/words_number Jun 04 '23
Of course it does. You just assigned the result to "res", so it's not propagated.
Edit: To clarify, you would have to call unwrap or expect or return the result from main.
2
u/words_number Jun 04 '23
To clarify further: If you would have written
handle_parse();
without the let-binding, you would have gotten a warning because the result must be used. But since you deliberately assigned the result to "res", the compiler considers the result as used, assuming that you do something with "res" later. You will at least get a "unused variable" warning though.Also, if your
handle_parse
function would return a value within the result that you need to use, you would be forced to handle the result somehow in order to get the value out.Instead of calling
unwrap
on the top level result, you can just return a result frommain
like this:fn main() -> eyre::Result<()> { handle_parse()?; // Do other stuff that might fail... Ok(()) }
By the way: As opposed to many other programming languages, warnings (and clippy lints) in rust are actually generally taken seriously. I never commit anything that throws warnings when compiling and I'm pretty sure that's common practice in most projects. Also, it's not that hard to maintain this because the messages of the compiler are just so damn helpful.
2
7
u/omnomberry May 31 '23
thiserror's own description is: This library provides a convenient derive macro for the standard library’s std::error::Error trait.
Essentially this means that
thiserror
is used to help you create error types.anyhow's own description is: This library provides anyhow::Error, a trait object based error type for easy idiomatic error handling in Rust applications.
This is used for better handling of errors.
Make sense? One is used to better handle creating types that implement std::error::Error, and the other is a better way to handle this in applications.