Error-chain is no longer maintained

This may be common knowledge, but it came to my attention today, so I figured I would mention it.

The error-chain crate, once a relatively common way to implement error types, has not been updated since last June. Pull requests have accumulated.

If somebody cares about this project enough to do the maintenance work, consider asking someone on the Rust team to make you a maintainer.

Otherwise, it would be nice if someone with access would update the docs to indicate the project is unmaintained and move it to rust-lang-deprecated.

New projects should probably not use error-chain. failure provides most, if not all, features that error-chain does, with a much cleaner interface, and is supported by the Rust team.

6 Likes

Thanks. Funnily enough 5 mins ago I posted a comment about error handling to Steve Klabnik's Roadmap 2019 thread on reddit.

Note that failure itself, while maintained, is still in a bit of a flux. Specifically, there are no immediate plans for 1.0 release. So, while using failure is probably better than using error-chain, it's not a clear-cut choice either. This comment provides a nice summary of the current state of affairs in error-handling.

5 Likes

I've used error-chain in most of my projects. I noticed it was producing deprecation warnings since 1.33, but had assumed they would be fixed.

Has anyone written a porting guide from error-chain to just the normal error handling? (I don't want to switch to failure or any others and get burnt again!)

My main reasons for using error-chain are that I can use ? for errors of different kinds in the same function, and for the bail! macro which I use a lot.

Also in BurntSushi's post he says "The downside to using Box<Error> is that it's probably not something one wants to use in a library." So I wonder what should one do in a library with just the built-in error support?

Personally, I would not attempt to write Rust error types without external library support. There's just too much boilerplate-y junk to consider.

A lightweight alternative to error_chain! and failure is the error_type! macro that I've seen in a few projects. I don't have much experience with it, but it seems to do the job.

But the way you do it is with the same patterns the various libraries tend to help with: define an enum for you local error type, and as external error types enter the project and need to be converted with ?, add a new variant that simply wraps that type, along with a From impl.

enum MyError {
    LocalError1,
    LocalError2,
    IoError(io::Error)
}

etc. Managing the descriptions is going to be a pain. Managing the cause chain is going to be so hard that it's not worth doing, but if you did want to add extra semantics to a wrapped error it could be something like

enum MyError {
    CorruptDatabase(io::Error),
    IoError(io::Error)
}

where CorruptDatabase can have its own description, and report the cause as the io::Error.

You just have to write all the impls by hand.

I'm now using a custom error for each project and putting up with the duplication.
See Failing to port from error-chain to std error handling and How to make a (simple?) macro.

1 Like

I used to use error-chain and failure, but I tend to just write my errors directly now, so I have control over how they work. A simple error would be something like

#[derive(Debug)]
pub enum MyError {
    CannotOpenFile(io::Error),
    CannotReadData(io::Error),
    // ...
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::CannotOpenFile(e) => write!(f, "error while opening file: {}", e),
            // ....
        }
    }
}

impl Error for MyError {
    fn source(&self) -> &dyn Error {
        match self {
            MyError::CannotOpenFile(e) => e,
            // ...
        }
    }
}

You don't have to impl the source method if you think the fmt msg is enough, in which case you'd have

impl Error for MyError {}

which is a bit shorter :slight_smile:

At the end, my blog post says this:

If you’re writing a library and your code can produce errors, define your own error type and implement the std::error::Error trait. Where appropriate, implement From to make both your library code and the caller’s code easier to write.

The blog post contains at least a few examples of this.

Today, I would probably not use any error handling helper library for a library. Generally, I do not think defining your own errors requires that much boiler plate. Defining the error variants themselves, along with how to convert them and how to show them, are all important aspects of your public API. (And perhaps more importantly, paying careful attention to this is crucial for choosing what exactly you expose. It can be very easy to accidentally incur a public dependency on another library by re-exporting one of its errors or exposing a From impl. It may instead make sense to not export such errors.) The "boiler plate" part of it is really the bits and pieces around it, e.g., writing out the impl From<...> for ... {}, writing out the impl fmt::Display for ... { } along with the corresponding match expressions.

I've done this many times myself. Here are some of them: (Some of this code still contains impls of cause or description, but you can drop the latter, and should implement source instead today, if pertinent.)

5 Likes

FWIW, I've recently switched from failure to regular std errors and cannot report many problems, except the lackluster behavior of Box<dyn Error>.

Since I'm not a library author, I can afford using err-derive (derives Display and Error) and derive_more (derives Display, From and much more) to deal with the boilerplate. Though I agree that the boilerplate is generally manageable even without these.

2 Likes

I've now switched to my own error module but I notice that it is slower than error-chain and I don't know why.
It is in the source for retest's xerror.rs file which I've also copied below since its only 100 lines.

Is there any way to make it faster (without making it too complicated!)?

use std::error::Error;
use std::fmt;
use std::io;
use std::num;

pub type XResult<T> = Result<T, XError>;

pub fn xerror<T>(message: &str) -> XResult<T> {
    Err(XError::new(&message))
}

#[macro_export]
macro_rules! xerr {
    ($x:expr) => {{return xerror($x);}};
    ($x:expr, $($y:expr),+) => {{return xerror(&format!($x, $($y),+));}};
}

#[derive(Debug)]
pub enum XError {
    Image(image::ImageError),
    Io(::std::io::Error),
    Json(json::Error),
    Log(log::SetLoggerError),
    ParseFloat(::std::num::ParseFloatError),
    ParseInt(::std::num::ParseIntError),
    Rayon(rayon::ThreadPoolBuildError),
    Retest(String),
}

impl Error for XError {}

impl XError {
    pub fn new(message: &str) -> XError {
        XError::Retest(message.to_string())
    }
}

impl fmt::Display for XError {
    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            XError::Image(ref err) => write!(out, "Image error: {}", err),
            XError::Io(ref err) => write!(out, "File error: {}", err),
            XError::Json(ref err) => write!(out, "JSON error: {}", err),
            XError::Log(ref err) => {
                write!(out, "Failed to set logger: {}", err)
            }
            XError::ParseFloat(ref err) => {
                write!(out, "Failed to read decimal number: {}", err)
            }
            XError::ParseInt(ref err) => {
                write!(out, "Failed to read whole number: {}", err)
            }
            XError::Rayon(ref err) => {
                write!(out, "Failed to create thread pool: {}", err)
            }
            XError::Retest(ref err) => write!(out, "{}", err),
        }
    }
}

impl From<image::ImageError> for XError {
    fn from(err: image::ImageError) -> XError {
        XError::Image(err)
    }
}

impl From<io::Error> for XError {
    fn from(err: io::Error) -> XError {
        XError::Io(err)
    }
}

impl From<json::Error> for XError {
    fn from(err: json::Error) -> XError {
        XError::Json(err)
    }
}

impl From<log::SetLoggerError> for XError {
    fn from(err: log::SetLoggerError) -> XError {
        XError::Log(err)
    }
}

impl From<num::ParseFloatError> for XError {
    fn from(err: num::ParseFloatError) -> XError {
        XError::ParseFloat(err)
    }
}

impl From<num::ParseIntError> for XError {
    fn from(err: num::ParseIntError) -> XError {
        XError::ParseInt(err)
    }
}

impl From<rayon::ThreadPoolBuildError> for XError {
    fn from(err: rayon::ThreadPoolBuildError) -> XError {
        XError::Rayon(err)
    }
}

I can't see any obvious problems with your code, so I can't say why the new version is slower. It'd probably help if you could show a comparison between the error-chain version and the new std-based one, together with some measurements that show the slowdown.

That being said, I think any concrete issues with migrating from error-chain are probably a little off-topic here. You might want to open a new thread with the details on your problem that I mentioned above.

So why not use Failure for libraries? It is that the failure::Failure trait becomes part of the public API of our error types, and we don't know if Failure is going to be around long term?

Because, for me, it's not just about the boilerplate. I like to wrap up underlying errors with ResultExt::context and I want backtraces for every underlying cause.

If someone is looking for another alternative, please check out my SNAFU crate:

A quick list of features:

  • Wraps underlying errors with additional domain-specific information
  • Provides backtraces when desired
  • Easily specify Display
  • Uses the Rust std::error::Error trait
  • Supports Rust 1.18+
  • Designed for use in libraries or applications

Using an example from above, the same functionality would look like:

#[derive(Debug, Snafu)]
pub enum MyError {
    #[snafu(display("error while opening file: {}", source))],
    CannotOpenFile { source: io::Error },
    CannotReadData { source: io::Error },
}

You'd use this as

fs::open(filename).context(CannotOpenFile)?;

However, I'd encourage adding more detail to the error message:

#[derive(Debug, Snafu)]
pub enum MyError {
    #[snafu(display("error while opening file {}: {}", filename.display(), source))],
    CannotOpenFile { source: io::Error, filename: path::PathBuf },
    CannotReadData { source: io::Error, filename: path::PathBuf },
}

And using it like:

fs::open(filename).context(CannotOpenFile { filename })?;
7 Likes

Firstly, as someone who appreciated error_chain when it was released, thank you @brson for building it, I personally benefited from learning from it's usage.

Second, for those saying that they're avoiding failure and using standard types, I'm curious if you support backtrace? This is my primary motivation for using failure at the moment.

2 Likes

Not in libraries, no. This is usually because libraries have targeted errors, and it's pretty easy to see where the error comes from.

For applications, sure, that's why I mention failure::Error in my blog post: its backtrace support is very nice. (snafu also supports this, which I'm in the process of experimenting with in ripgrep core.)

1 Like

Isn't this a problem when the structure is:

  • MyApp
    • CoolMetaLibrary
      • NestedLibrary

And then NestedLibrary encounters an error.

If NestedLibrary has not captured a stack trace at the location of the error, the best you can do is hope that CoolMetaLibrary did. If it hasn't, then your trace will only point to the location in MyApp where you call into CoolMetaLibrary, which you probably already knew.

I actually hope for a world where error types are fine-grained enough to identify a line of source code from a specific error, but I fear that such a world isn't easily attainable.

1 Like

I do think it is a potential problem in theory, and I still think it's good for the error ecosystem to support it.

Honestly though, even though I introduced backtraces into errors in error-chain, I have never used error backtraces for any purpose. I have to imagine that people supporting large codebases would find it useful, as long as they are threaded all the way through the chain, which they almost assuredly will not be considering the fractured state of the ecosystem.

4 Likes

Imagine that NestedLibrary error contains some path to the file that is missing or corrupted. If CoolMetaLibrary error does not point to the NestedLibrary error, then it may be hard to guess that MyApp error is caused by the corrupted file.
It happens to me all the time.

1 Like

This is a different problem than the one that a backtrace solves. A backtrace will identify the line of code the error occurs at and all the intermediate functions. It will not include the file name you attempted to open.

“Nesting” errors does include what you are talking about, and most (all?) error management libraries perform that goal. ( of course, I think SNAFU is best :grin:)

1 Like

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