Announcing `err-derive` - yet another error handling library

err-derive

A failure-like derive macro for the std Error.
The source code is mostly copied from failure-derive.

Motivation

Why yet another error handling library? There already are dozen others, the most popular being:

The former provides a nice #[derive(Fail)] macro, but it uses its own error type (Fail) and its usage is rather discouraged since std::error::Error is getting fixed to provide the same benefits as Fail.

error-chain does support std::error::Error, but it uses a declarative for generating the error implementation which makes the syntax too obscure in my opinion.

This crate tries to combine both advantages:

  • work with std::error::Error
  • provide a custom #[derive(Error)] that works just like failure-derive

err-derive is compatible with std, including the recent change to deprecate Error::cause in favour of Error::source, and provides an easy syntax for generating the Display and Error boilerplate (the latter being 99% copied from failure-derive).

Features

err-derive can be applied to your error struct / enum and does the following for you:

  • Derive Display implementation
  • Derive Error implementation (implementing source to return the cause of the error)

Planned features

  • Derive From<OtherError> implementations

Usage

Cargo.toml:

[dependencies]
err-derive = "0.1"

Rust code:

#[macro_use]
extern crate err_derive;

use std::error::Error;
use std::path::PathBuf;

#[derive(Debug, Error)]
pub enum FormatError {
    #[error(display = "invalid header (expected: {}, got: {})", expected, found)]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error(display = "missing attribute: {:?}", _0)]
    MissingAttribute(String),
}

#[derive(Debug, Error)]
pub enum LoadingError {
    #[error(display = "could not decode file")]
    FormatError(#[error(cause)] FormatError),
    #[error(display = "could not find file: {:?}", path)]
    NotFound { path: PathBuf },
}

impl From<FormatError> for LoadingError {
    fn from(f: FormatError) -> Self {
        LoadingError::FormatError(f)
    }
}

fn main() {
    let my_error: LoadingError =
        FormatError::MissingAttribute("some_attr".to_owned()).into();

    print_error(&my_error);
}

fn print_error(e: &dyn Error) {
    eprintln!("error: {}", e);
    let mut cause = e.source();
    while let Some(e) = cause {
        eprintln!("caused by: {}", e);
        cause = e.source();
    }
}

Credit

Credit goes to @withoutboats and other contributors of failure.

14 Likes

Nice work! Does it allow deriving Display with errors that contain Path/PathBuf?

1 Like

Yes, it works just like with failure:

#[derive(Debug, Error)]
pub enum FormatError {
    #[error(display = "invalid header (expected: {:?}, got: {:?})", expected, found)]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error(display = "missing attribute: {:?}", _0)]
    MissingAttribute(String),

}

#[derive(Debug, Error)]
pub enum LoadingError {
    #[error(display = "could not decode file")]
    FormatError(#[error(cause)] FormatError),
    #[error(display = "could not find file: {:?}", path)]
    NotFound { path: PathBuf },
}

Looks nice. I'm looking forward to From implementations, as that's the main thing that's keeping me on quick-error.

2 Likes

What @BurntSushi means is that you need to call display() on the path to make it implement the Display trait.

1 Like

Thanks, I didn't even know about that method.
I see two ways to solve this:

  • allow specifying a function that gets applied to a field (fmt = "PathBuf::display")
  • allow an expression (expr("self.path.display"))

The latter seems more flexible, but I think expressions in derives are discouraged. The hardest part here would be figuring out a clean syntax. Any ideas?

Hello

I always thought the plan for failure was to eventually converge somehow and be at thin wrapper around std::error::Error. Do you know if it is still true?

Anyway, there's one more thing that is very handy about failure ‒ it's Error type that can wrap whatever error, provides backtraces „out of the box“ and allows adding layers of context without explicit support from the error types being used:

Are there plans for something like that here too, except implementing the Error trait?

1 Like

Hello @vorner!

No, I unfortunately cannot answer that.
I just want to create nice APIs and a #[derive(Error)] is what I was missing for that.

Yes, I know about those, but they're outside of the scope of err-derive.
That is actually what this PR to the Amethyst game engine is doing; maybe we should move that into an external crate?

cc @udoprog

I guess another crate would be fine (I just expected that would be failure 1.0 eventually, but nothing against getting there by other means).

I wonder if that much is even needed, or if using type Error = Box<dyn std::error::Error> and struct ContextError { inner: Error, msg: String } (or struct ContextError<M: Display, E: std::error::Error>(M, E)) would actually be good enough for most of the needs.

1 Like

If failure can provide that, even better! No need to reinvent things. I don't use backtraces with errors personally, but let me check if it's compatible.

EDIT:

Yes, you can use std::error::Error using failure::Error::from_boxed_compat. However, that defeats the purpose of err-derive since it means you're exposing API of failure.

If you just want to generate a backtrace, you can do that simply by using backtrace::Backtrace::new().

I didn't mean wrapping it in failure, I meant going without it.

Anyway, I think I'll put some PoC of something together soon, maybe tomorrow and show it here ‒ let's see how far we can get.

1 Like

OK, so I put something together: https://github.com/vorner/errutil (it's missing all the license and other formalities for now, I'll put the usual stuff in eventually if this is supposed to live).

Anyway, this contains:

  • The type Error = Box<...> convenience type.
  • Extension traits for error and result, making it possible to enrich them by more levels of context and a backtrace.
  • I'm not happy with the WithBacktrace type, though, because it mandates dynamic dispatch inside. I'd much prefer to have a WithBacktrace trait instead of type and anyone could implement it on their own (and have one type or even a generic type provided in the library). But for that to be of any use at all, Rust would have to support downcasting from one trait object to another and it doesn't seem to be currently possible :-(.

What do you think? What would be the next steps? Try to release it (and over time add all the other convenience stuff failure provides), or try to offer it if failure would like to do something like that? The backtraces here are strictly less capable, but failure is already part of lang-nursery and has a known name.

1 Like

I think the best solution for now is that libraries either use their own "error utils" or only use existing crates internally. The error story is currently worked on in std and I don't think it makes much sense exposing any of that in a public API right now since it can change once things land in std.

So for applications that might be a useful tool, but I personally wouldn't use it as public dependency of a library. That's also why we are going to keep amethyst_error as it is.

I'm not 100% sure I understand your concern there. Sure there's some discussion around what happens with error handling, but std also promises backwards compatibility. So I think re-exporting API from std is perfectly fine. Actually, it seems std is the only thing that one kind of must rely on. Or maybe you mean something else?

I get that the rule of thumb for a library is to provide some concrete error type, not Box<Error> so it's clear what might or might not go wrong. But apart from that (or when eg. accepting errors from a user callback), do you see some other problem?

Maybe I misunderstood. Are you suggesting that errutil should only be used as internal dependency?

Not exactly. What I'm trying to say:

  • The WithBacktrace type was a failed experiment. If anything is to come from errutils, that is to be thrown away.
  • The errutils::Error is a type alias for Box<std::error::Error + Send + Sync>. So it's fine to expose that through public API, because it is not a new type, it is just a re-export of type coming from std. If I decide to drop the dependency on errutils and change it to that type directly, nothing in my public API changes.
  • The WithContext type is a way to define my own error type (by wrapping another one). Does it really matter how I create my error types, if by eg. error-chain or by providing pub type MyError = WithContext<SomeOtherError>?

Other things that could be added to errutils are equivalents to eg. failure::bail! or failure::err_msg. These generate anonymous errors wrapped inside the Error type, so this probably even can not be exported through public API.

In other words, what I'm trying to say is there's not much way to expose more than Error itself and that's a type from std. Is that internal or external dependency, then?

It does indeed. If you have a complicated error you cannot downcast it. If you only have an enum, You could cast it to the desired type.

FWIW, the derive_more crate can be used in addition to failure or err-derive to derive From.