On error handling

Over in the top and flop topic, people have brought up error handling as both a pro and a con. So I wondered if people would be willing to go into more depth here.

I could be wrong but I think that most people like the basic way Rust handles errors. A function simply returns a Result which can then be matched on etc.

However I think the problem is in how you design a system of errors over the whole library or application? There are a range of different crates that offer to help with this in large or small ways (e.g. failure, quick-error, error-chain, etc).

Am I barking up the right tree here or am I way off?

6 Likes

I’m on the top side.

I use mainly quick-error. Its syntax is a bit quirky, but it does provide “glue” for error types with minimal effort.

I guess to some having many crates and many ways to implement errors is an overwhelming mess, but IMHO that’s also a strength: for every library or subsystem in my program I can list exactly all the ways in which it can fail.

And I can decide whether I want error types to be minimal — just bare enum that compiles down to a small integer — which is great for efficiency of frequently called functions. Or I can return a heap-allocated error object with all the details and the entire chain of related errors. Both are valid depending on context. When I’m lazy Err("string")? works, too.

3 Likes

Also on the top side, by far.

I generally don’t use any of the various error handling helper libraries. Writing out error definitions is not in any way a significant source of where my time is spent. For the most part, they are written out once as I come up with different error cases, and that’s it. IMO, the killer feature of a crate like failure is its support for backtraces which are very useful. My hope is that those will come to std at some point.

6 Likes

Well, what I find to be beautiful is that you have a few core features that make up a great experience:

  • The std::error::Error trait is pretty universal, allowing you to just pass a Box<dyn Error> around, and not specialized types.
  • Which leads to the second point the ? operator, which can intelligently figure out how to move your errors around. This goes hand in hand with the Box<dyn Error> point because you can have a situation like this:
struct MyError;
impl std::error::Error for MyError {/**/}

fn foo() -> Result<(), MyError> {/**/}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    foo()?;
    Ok(())
}

where the MyError will be auto-coerced into a Box<std::error::Error>

2 Likes

There’s lots of complaint (flop) about boilerplate with rust error handling, in particular custom error types, but someone should compare the volume of boilerplate with languages that have exceptions. I’m top as well and I didn’t come from Go (lang).

I’ve been working on a longer blog post on this (that I’ll cross-post here on users), but to summarize: I think I’m doing better (top), with error handling now that I’ve stopped using any of the various error helper crates and stuck with only libstd features. For custom error types, I like enums, and what I end up writing is similar but not identical to what the quick-error macro’s produce.

2 Likes

I have 2 main concerns about error handling.

The first one is about documentation : as quite a beginner, searching for a proper tutorial on how to implement a nice error chain and it has been a terrible experience pretty much. No tutorial was saying the same thing, it was painful, really. Use this crate, use this one, use no crate, use 2 crates, come on. The mere fact that everyone seems to advise for different techniques shows that there might be some work to do here, I think.

The other one is the lack of context of standard errors. Their message is generally not sufficient to understand what’s going on. Error::NotFound doesn’t say WHAT has not been found, for instance. If one tries to open a directory as if it was a file, one will have the error message “Errno number - Is a directory”, which doesn’t mention the path in question.

In the end, when you’re in a method of your own, it’s no problem, you can add context. But when you’re using a method of a crate that can raise the same error at different location of that method (e.g. recursive methods to walk dirctories ), if the creator of the crate didn’t add context, you’re screwed. You can create your own custom message, but in the end, it will be something like “a path wasn’t found” or “one of the path was expected to be a file but it’s a directory, I have no idea which one though”, which is a pity.

4 Likes

Thanks for your thoughts everybody. On the question of tutorials, does anyone know of any that can address the multiple ways error chains and context can be handled?

Or is this something beginners have to wade through for themselves?

Hi,
For error chains I am using https://docs.rs/error-chain/0.12.0/error_chain/index.html
and see also https://rust-lang-nursery.github.io/rust-cookbook/errors.html
And for error in general https://github.com/ctjhoa/rust-learning#best-practicesstyle-guides of course the book has also a good section for that.

Hi,

For me The best resource is https://blog.burntsushi.net/rust-error-handling/
and the failure crate (https://github.com/rust-lang-nursery/failure).

TL;DR: You define 1 enum of errors for you entire program / lib which derives Fail (and all you Result functions returns this error type).

I’ve used error-chain, quick-error, failure but now with std::error::Error essentially being a marker trait, I don’t use any of them. Instead of an “error crate”, the derive_more crate lets me define and manage strongly-typed error types with no boilerplate at all.

Not having to type and maintain an impl Display block keeps things clean and succinct (custom Display trait can be impl’ed via #derive attribute when using derive_more).

Even foreign error declarations From impls can be handled automatically–it’s hassle-free, very nice, and you still end up with strongly typed per-crate custom errors.

I think I may write up a blog post to expand on this, because it’s an area that I think a lot of people are interesting in understanding better. But here’s what it looks like in my “quickstart” (empty app/lib) template:

use derive_more::*;

#[derive(Debug, Display, From, PartialEq)]
pub enum Error {
    #[display(fmt = "{}: {:?}", "msg::ERR_NOT_AN_INT", "_0")]
    NotAnInt(std::num::ParseIntError),
}

Note:

  • #[derive(From)] attribute, above. Whenever I use a foreign type (std::num::ParseIntError in the example above), derive_more creates a From impl automatically.
  • #[display()] attribute, above. it includes the ability to custom format, reference const values, and error payload tuple fields (using the notation _n instead of .n)

There are many more features, but its the most succinct, expressive solution I’ve found. It even helps tackle problems like std::io::Error not implementing PartialEq, for example, although I’ll save that for another post.

In the end, the error definition exercise becomes just listing the variant, any payload types you want it to accept, and a custom display formatter, if you want one)–this is what I’ve been looking for for a long time.

With this, #[must_use] on Result, the Option type and the error-check operator (?), Rust error handling is definitely “Top” for me as well. :slight_smile:

4 Likes

So, @U_007D you prefer derive_more to quick_error ? Its .context() feature is quite neat (the only main thing lacking, imho, is support for trailing commas)

I do. In addition to the succinctness benefit mentioned above, my display strings are all resourced (defined in a separate file). Even though they are const, quick-error was unable to use these at the time I worked with it (same for the others–they all required literals). derive_more supports this.

In terms of context, I try to design my Error to contain all relevant context, so that a .context() isn’t necessary. By way of a simple example, instead of defining an Error::FileNotFound, I would define an Error::FileNotFound(String) or Error::FileNotFound(Path) such that the context of what file was not found is fully encapsulated by the error itself.

1 Like

Small note: be careful with this. Adding them for error types defined in std is fine, but if it’s on an error type from a crate you’re using, then this will make that crate a public dependency. That might be OK, but it should always be done thoughtfully.

3 Likes

I’m using a very similar approach, except I use err-derive (announced here) for the Display implementation. Its advantage is that it also supplies a #[cause] attribute to customize the only Error trait method that still matters.

With it, your example would look roughly like this:

use derive_more::From;
use err_derive::Error;

#[derive(Debug, Error, From, PartialEq)]
pub enum Error {
    #[error(display = "{}: {:?}", "msg::ERR_NOT_AN_INT", "_0")]
    NotAnInt(#[cause] std::num::ParseIntError),
}
2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.