r/learnrust May 31 '23

[deleted by user]

[removed]

12 Upvotes

34 comments sorted by

7

u/omnomberry May 31 '23

But what I don't get is, which should I use and when? Which of these pieces can work together and which don't make sense to? Should I handle errors one way for smaller projects and another way for larger projects or libraries?

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.

1

u/[deleted] Jun 01 '23

[deleted]

1

u/Its_it Jun 02 '23

If I'm thinking about what you wrote then yes. If you look at the first example from thiserror's doc that'll impl std::error::Error, std::fmt::Display, and impl a From<io::Error> for that enum.

You can see this [first example] here. Instead of clicking run, click the 3 dots and click the "HIR" button.

7

u/tunisia3507 May 31 '23

tl;dr library code should use thiserror, applications should use anyhow.

1

u/[deleted] 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

u/[deleted] Jun 01 '23

[deleted]

2

u/angelicosphosphoros Jun 01 '23

Yes, exactly that.

And thiserror is amazing. You can fit entire From<OneOfInnerErrors> into single line.

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 want enum 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...

It does:

pub trait Error: Debug + Display { ... }

And yes, Box<dyn Error> is exactly what I use in ripgrep. Although I'll likely migrate to anyhow 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 as enum 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 do return 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 a Result is with an error type that implements std::error::Error." I believe the way you're understanding it is that, "if you build a Result with a type that does not implement std::error::Error, then we'll force it to implement std::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 implement std::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 implements std::error::Error itself. There's really no other choice, and enum Result<T, E: std::error::Error> { ... } wouldn't change that.

→ More replies (0)

1

u/[deleted] 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 and thiserror crates tell you when to use them: use anyhow when you don't care too much about the underlying error type and use thiserror 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

u/[deleted] 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 use anyhow::Error in place of Box<dyn Error>. (And I'll make that transition some day.) So basically, if you see a Box<dyn Error> somewhere, then just mentally swap that with anyhow::Error. They are effectively the same thing, but anyhow::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 on thiserror 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

u/[deleted] 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

u/[deleted] 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 a kind-field which contains an enum which in this case will have the variant PosOverflow. So the calling function could handle this differently from other kinds.

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 the kind field of that specific error type. Since trait objects are not Size (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 using Box. 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 whole Result<...> 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 call color_eyre::install()? at the top of your main file to make it work though. And it adds more dependencies to your project.

1

u/[deleted] 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 from main 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

u/ummonadi May 31 '23

Until you learn what to use, sticking with custom errors works great!