Announcing error-chain, a library for consistent and reliable Rust error handling

Both as far as I can tell. There is an actual line number / symbol backtrace which uses the backtrace crate, but there is also the 'semantic' backtrace of cause-s which you can [iterate]((https://github.com/rust-lang-nursery/rustup.rs/blob/master/src/rustup-cli/common.rs#L344) over using iter

If this relies on stack walking, then I would expect it to be somewhat inaccurate in Release mode or when Debug symbols are stripped.

Still, having used a similar solution C++, it's so much more useful than nothing!

@dpc I believe it works on stable, but have not actually tested personally. I know rustup as a whole can build on stable but I haven't done it.

@nikomatsakis Actual stack traces, with whatever information the backtrace crate can pull out of the debugging info.

Yeah, it's going to give you whatever quality of backtrace libbacktrace is capable of.

Sorry if it's stupid question, is there any reason you can't make Error generic over ErrorKind ? (I guess there some coherence issues?)

If it's not possible, same question for ChainErr trait. It will be tricky if you would ever need to use chain_err from two different errors in the same module (Not sure how common is it).

And wrapping Result trait looks like needless. Making a type alias is just a one-liner in plain Rust (i.e. less magic in macro is good thing in my opinion)

Is it possible to disable the backtrace feature? After reading Joe Duffy's thoughts on error-handling, I am fairly well convinced that backtraces are only of interest for debugging fatal errors, which is very separate from programmatic handling of recoverable error conditions. I can certainly see how a backtrace would be interesting during development work while recoverable error cases are not yet handled, but I don't think "generate a backtrace by default whenever we stray from the happy path" is likely to be a helpful mindset for making a robust final product.

(Ideally, I'd like to be able to enforce a project-wide configuration that would have backtraces included in debug mode but not release mode.)

Forgive me, it's been a month since I worked on this code so I've forgotten some, but it looks to me like there would be coherence violations in the From conversions, e.g.

impl From<$link_error_path> for $error_name {
    fn from(e: $link_error_path) -> Self {
        $error_name($error_kind_name::$link_variant(e.0), e.1)
    }
}

I tried this to start and couldn't formulate it in a way that worked with coherence. As long as you only create one type of error per crate the scenario you describe of needing multiple ChainErr shouldn't happen.

It's not a wrapper, it's a type alias. A person could write it as a one liner, but I figured since I was doing all the other definitions, and it's common practice to do the type alias, the macro should just do it.

It's not currently, but I can see it might be worth while. Personally, even if it's not useful in some scenarios I wouldn't have a desire to turn it off. The reason I see to disable it is to save the work of generating it, but I also consider it a design tradeoff that (in this library at least) performance on the error path is not critical (lots of boxing in this strategy).

I think I agree with that too, but I would also suggest that backtraces are useful for debugging unexpected errors. It's reasonably common to get bug reports that show an error that my program printed, and for me to have no idea why that would happen. If I can say 'will you run that again with backtraces on?' it will help me figure out what happened.

Oh, yes, absolutely. (That's actually a much better statement than my "only of interest for debugging fatal errors"--I think what I'm really trying to get at is unexpect errors should be fatal if possible.) But how can an error be unexpected if it's part of the signature of a method? If you don't expect a particular error condition, panic when you see it (again as per Duffy's advice). In my ideal world, once a software product is ready for release, errors that can be handled are, while errors representing true unforseen bugs and conditions that were (incorrectly) assumed to be impossible are "handled" by panicking (if indeed they're handled at all, since the whole point is that this category of errors can't be predicted in advance).

I suppose I can think of a very few cases where generating some backtrace information and propagating it via error-types might be useful, even in release mode. One is if there's some way to panic inside the error-handling code several levels up the call-chain; in this case you'll lose some potentially-crucial backtrace information if the "semi-happy-path" error-recovery mechanism doesn't propagate the trace info "just in case". But that seems (prima facie) like a pretty limited scenario to me.

Awesome! I just converted one of my codegen projects with a bunch of different possible error types over to error_chain, it was really simple. Not a single impl Display in sight :smile:

3 Likes

Glad to hear it!

I just wanted to add my kudos, this is excellent. I started working on a much lamer version of this macro, and am in the progress of switching. Excellent job.

A note to other beginners, since this generates so many structs and impls, you might be wondering (like #[derive(..)]) how to "view" the structs. The easiest method i've found is to rely on cargo doc --open and then use that to browse all of the code interfaces. I've started using this instead of just jumping around to code at this point.

Okay, I'm here again seeking for alternatives.

Actually, we already have error chaining in quick-error using into() and context(), and adding traceback manually is easy: here is a full example and a diff to example without tracebacks. The interesting part of example:

quick_error! {
    #[derive(Debug)]
    pub enum Error {
        Io(err: io::Error, path: PathBuf, tb: Backtrace) {
            display("could not read file {:?}: {}\n{:?}", path, err, tb)
            context(path: &'a Path, err: io::Error)
                -> (err, path.to_path_buf(), Backtrace::new())
        }
...

What's notable here:

  1. You can just add a var and a Backtrace::new() call. Easy.
  2. You can add backtrace only for some cases, i.e. unexpected errors. Other conditions don't have to pay for boxing.
  3. The display handler is obviously wrong (includes a traceback), it's just for showcase here
  4. Traceback objects may be more complicated like Option<Arc<Backtrace>> but in can be type-aliased. And constructor (which checks environment, for example) might be just plain function, no need for a complex macro.

What we want is a pretty printer which walks the causality chain and prints errors with tracebacks. Making a pretty printer is easy as long as we have a trait that exposes all needed information.

Ideally, we would have Error::traceback() in the std::error::Error similar to Error::cause.

If we are out of stdlib we may add a TracebackExt trait:

trait TracebackExt {
  fn traceback(&self) -> Option<&Backtrace>;
  fn cause(&self) -> Option<&Error + TracebackExt>;
}

It's easy to get this trait implemented in a way similar to how cause works in quick-error (the only issue is that I don't wan't quick-error to depend on backtrace crate)

What do you think?

For those interested, I just converted to using this library (took a while, I had a lot of errors defined!)

A couple of things I noticed:

  1. you will run into: error: recursion limit reached while expanding the macro 'quick_erro', the limit of errors looks to be 10. Edit: it looks like this is inconsistent, 10 in some cases, others allow more, may be some other bug that I'm missing.

  2. I've run into an issue where into() isn't really good enough, especially when using chaining. I decided to just do Error::from(ErrorKind::Kind) where needed.

I published a new build, 0.2.1. The only change is that now error chain lives in its own repo, so the doc links have changed.

I think I'd like to log a message at the point where the error is first noticed (eg, soon after the failed syscall), even if it's later handled short of terminating the program. I have some boilerplate to do this today, which could potentially be converted to macros. Is this handled by error_chain?

(Apologies if this is in the docs somewhere and I missed it.)

It doesn't today but it would be easy to add at the same place the backtrace is being generated. Patches welcome!

I've just released error-chain 0.2.2. In this release the types { } section may be omitted, and I have added a quick start section to the README.

0.2.2

Contributors: Brian Anderson, Jake Shadle, Nate Mara

3 Likes

I've just discovered that error-chain seems to not work on 1.10 stable: error-chain doesn't work on stable 1.10! · Issue #12 · rust-lang-deprecated/error-chain · GitHub

This is surprising to me since at one point rustup did build on stable.

error-chain 0.3.0 is out. In this revision, foreign errors are stored in the ErrorKind variant instead of boxed into Error's 'cause' field, and the Display impl shows output of the foreign error's Display impl instead of the simple description.

0.3.0

Contributors: Brian Anderson, Taylor Cramer

There's a PR open against error-chain that introduces a major breaking change, and I'm curious what people think.

So after the last release that changes how foreign links are represented, their 'description' is basically useless. As a reminder the description is the string here:

error_chain! {
    ...

    foreign_links {
        ForeignError, ForeignErrorKind, "description"
    }
}

So this PR removes it completely, with no backwards-compatibility considerations. My inclination is to just do it, along with a major version bump (0.3 -> 0.4 - cargo considers minor bumps less than 1.0 to be incompatible). ISTM that this library is immature enough, and has few enough users that when they do decide to upgrade it's not such a big deal to make the changes. But it may be possible to have the macro continue parsing the description and throw it away. Any opinions?

I think this breaking change is a good idea: I've personally never been 100% sure as to the purpose of the description. It somehow always seemed "extra" to me in the foreign_links section. And as the PR author mentions, fixing the code that'll break because of this change isn't too difficult.