Why can't I `impl<E> From<E> for EzError where E: StdError + Send`?


#1

I’ve spent a lot of time now trying to make libraries that simplify Rust’s error handling, and am super frustrated. It feels like things fit together in ways that require needless complexity for the end user. I am constantly being thwarted and forced to add extra conversion methods and special cases to work around the type system. You may be aware of my error-chain crate, which I think is the easiest way to deal with errors in Rust today - and it is still really complicated and requires users to understand how to do conversions correctly in several cases.

What I’ve been trying to do today is create the simplest possible library for dealing with errors correctly, one that doesn’t require newbies to understand details of all the traits and conversions around Error. The obvious way to do this is to use boxed Error + Send everywhere, but I can’t figure out how to do it.

Here’s some really simple code that would make ? automatically convert Result of any type implementing Error into a local error type. Of course it doesn’t work.

use std::result::Result as StdResult;
use std::error::Error as StdError;
use std::io::{self, Write};
use std::fmt;

pub type EzResult<T> = StdResult<T, EzError>;

#[derive(Debug)]
pub struct EzError(Box<StdError + Send>);

impl StdError for EzError {
    fn description(&self) -> &str { self.0.description() }
    fn cause(&self) -> Option<&StdError> { self.0.cause() }
}

impl fmt::Display for EzError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) }
}

impl<E> From<E> for EzError
    where E: StdError + Send
{
    fn from(e: E) -> EzError {
        EzError(Box::new(e))
    }
}

Try to compile this and there’s a conflicting implementation in core. What is that impl?

Is there any way to make ? automatically box any Error?


Brson's "someday" list
Brson's "someday" list
#2

Probably this one:

libstd/error.rs:impl<'a, E: Error + 'a> From for Box<Error + 'a> {

#3

Oh, hey, that highlights what may be the biggest problem with the Error design - not requiring Send. If that impl had the Send bound on it presumably I could do something generally useful with it. Types that don’t implement Send in Rust are special - and hostile to other types.


#4

Perhaps I can try to use that impl to stuff types into thread-local storage and create a sendable handle to them…

Or maybe just convert them to strings, though that destroys chaining.


#5

OK, I know. I can convert all errors to strings while simultaneously traversing the chain and converting the entire chain to strings. Converting all errors to being stringly-typed but preserving the causes. Maybe even stuff type-id’s into them for doing type-based error dispatch.


#6

Except the error is "conflicting implementations of trait std::convert::From<EzError> for type EzError", which matches the identity impl.


#7

Here’s another desirable thing you can’t do because of coherence and I don’t understand why:

use std::result::Result as StdResult;
use std::error::Error as StdError;
use std::io::{self, Write};
use std::fmt;

pub fn ez_main<F>(f: F)
    where F: Fn() -> EzResult<()>
{
    if let Err(e) = f() {
        let _ = writeln!(io::stderr(), "error: {}", e);
    }
}

pub type EzResult<T> = StdResult<T, EzError>;

#[derive(Debug)]
pub struct EzError {
    msg: String,
    cause: Option<Box<StdError + Send>>,
}

impl StdError for EzError {
    fn description(&self) -> &str { &self.msg }
    fn cause(&self) -> Option<&StdError> {
        self.cause.as_ref().map(|e| &**e as &StdError)
    }
}

impl fmt::Display for EzError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.msg.fmt(f) }
}

impl EzError {
    pub fn new<E>(e: E) -> EzError
        where E: IntoEzError
    {
        e.into_ez_error()
    }
}

pub trait IntoEzError {
    fn into_ez_error(self) -> EzError;
}

impl<'a> IntoEzError for &'a str {
    fn into_ez_error(self) -> EzError {
        EzError {
            msg: self.into(),
            cause: None
        }
    }
}

impl IntoEzError for String {
    fn into_ez_error(self) -> EzError {
        EzError {
            msg: self,
            cause: None
        }
    }
}

impl<E> IntoEzError for E
    where E: StdError
{
    fn into_ez_error(self) -> EzError {
        panic!()
    }
}

I can’t create one function that accepts both &str and any impl of StdError. Is there any way to do that?


#8

I can’t find any way to create a From implementation that accepts any type that implements Error (even as a reference), and that also works with try!. Without this ability itseems impossible to create an error handling library that doesn’t require constant attention to conversions. That is, when dealing with ?/try! today every input error type must have an explicit conversion to the local error type. It’s horrible for newbies. Can anybody tell me how to write a From conversion from a generic Error type that works with try!?


#9

You can’t, if the impl may apply to the source type as well, due to the identity impl<T> From<T> for T.

Even worse, EzError(Box::new(e)) would result in the identity impl suddenly allocating an unnecessary linked list, instead of doing nothing every time it’s called.

You would need some kind of variant of specialization, or a where E != EzError bound (cc @nikomatsakis).


#10

Is it true that there is a need in a call site invisible “type-erasing” conversion? Looks like Box<Error> is difficult to introspect ( https://users.rust-lang.org/t/cant-to-introspect-error-cause/6536 ), so perhaps it makes sense to make a call site a bit loud?


#11

You can get this to work, it just requires nightly and horrible hacks:

#![feature(optin_builtin_traits)]
use std::result::Result as StdResult;
use std::error::Error as StdError;
use std::fmt;

pub type EzResult<T> = StdResult<T, EzError>;

#[derive(Debug)]
pub struct EzError(Box<StdError + Send>);

impl StdError for EzError {
    fn description(&self) -> &str { self.0.description() }
    fn cause(&self) -> Option<&StdError> { self.0.cause() }
}

impl fmt::Display for EzError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) }
}

mod private {
    pub trait NotEzError {}
    impl NotEzError for .. {}
    impl !NotEzError for super::EzError {}
}


impl<E: private::NotEzError> From<E> for EzError
    where E: StdError + Send + 'static
{
    fn from(e: E) -> EzError {
        EzError(Box::new(e))
    }
}

This really is the big (imo) motivation for either negative reasoning or the lattice specialization rule (both would allow one to fix this in a slightly different way).


#12

In the past there have been problems with negative trait bounds because adding a trait implementation would become a breaking change (sort of, see RFC #1658). However, negative equality bounds don’t have this problem at all.

Also, a more general (unstable) solution to the above problem is:

#![feature(optin_builtin_traits)]
pub trait NotSame {}
impl NotSame for .. {}
impl<T> !NotSame for (T, T) {}

// Example: usage:
trait Trait<T> {}
impl<T> Trait<T> for T {}
impl<A, B> Trait<A> for B where (A, B): NotSame {}

#13

Sigh… Auto traits don’t work this way, they cannot be used to emulate specialization.

#![feature(optin_builtin_traits)]
pub trait NotSame {}
impl NotSame for .. {}
impl<T> !NotSame for (T, T) {}

struct Check<T: NotSame>(T);

fn main() {
    // (u8, u8) != u16, but ((u8, u8), u16) doesn't satisfy NotSame
    let a: Check<((u8, u8), u16)>; // ERROR trait bound is not satisfied
}

#14

I really hope there is a solution to this at some point, my favourite being negative trait equality bounds (impl<E> From<E> for EzError where E: StdError, E != EzError). For many novice Rust programmers, handling errors via panics is the only option, because implementing proper error types with the the correct conversion logic is just too difficult.

IMO this is a much more pressing problem than making error handling prettier via ?. What good is it to shave off a few characters in a method, if you then have to go and implement an error type, along with impls for Error, Display (ugh), Debug (derive, but still…) and a bunch of From impls.

Imagine someone new to the language just coming in and wanting to write a function which does some IO and then tries to parse the result to a UTF8 string, and does this with proper error handling, without any panics. They will have to learn about fmt, which in return requires them to learn about traits, the Write trait, the write! macro, macros, derive, then come the From trait, generics, generic traits, and so on and so forth. This is all really complicated stuff, which takes a while to wrap your head around. Imagine someone coming from a dynlang like Ruby or Python who has never really worked with a static type system trying to understand what From even does and how to write implementations for it.

In the end they’ll end up with a two line function and 30 lines of boilerplate.


#15

Where Send isn’t required the result isn’t too bad, but it could certainly be streamlined (e.g. a built-in boxed Error type). What I came up with was this (full source):

/// Our custom result type
pub type Result<T, E = Error> = result::Result<T, E>;

/// Our custom compound error type
pub type Error = Box<ErrorTrait>;

pub use std::error::Error as ErrorTrait;

then implementing fmt::Display and ErrorTrait (description function) for every error type.

The disadvantage of boxed errors is that special handling for specific errors is more difficult, the advantage is that generic propagation of any error is much easier (this still leaves the question of whether propagation of a generic Error object is more useful than a panic, now that panics can be caught — perhaps the best use is with something like enum SomeError { ErrorA, ErrorB, Other(Box<std::error::Error>) }).


#16

This isn’t specialization (there’s no overlap) but yeah, I didn’t test with complicated types. You can make it work but it starts to look nasty:

pub struct Pair<A, B>(A, B); // Don't use this in your types...
pub trait NotSame {}
impl NotSame for .. {}
impl<T> !NotSame for Pair<T, T> {}

// Handle unsized wrappers one-by-one...
impl<T: ?Sized> NotSame for Box<T> {}
impl<'a, T: ?Sized> NotSame for &'a T {}
impl<'a, T: ?Sized> NotSame for &'a mut T {}
// etc...

Here’s the full example I’ve been playing with:

#![feature(optin_builtin_traits)]

use std::error::Error;

pub struct Pair<A, B>(A, B); // Cannot be constructed...
pub trait NotSame {}
impl NotSame for .. {}
impl<T> !NotSame for Pair<T, T> {}

// Handle unsized wrappers one-by-one...
impl<T: ?Sized> NotSame for Box<T> {}
impl<'a, T: ?Sized> NotSame for &'a T {}
impl<'a, T: ?Sized> NotSame for &'a mut T {}
// etc...

enum MyResult<T, E> {
    Ok(T),
    Err(E),
}

enum MyOption<T> {
    Some(T),
    None,
}

impl<T1, E1, T2, E2> From<MyResult<T1, E1>> for MyResult<T2, E2>
    where Pair<MyResult<T1, E1>, MyResult<T2, E2>>: NotSame,
          T2: From<T1>,
          E2: From<E1>,
{
    fn from(r: MyResult<T1, E1>) -> Self {
        match r {
            MyResult::Ok(v) => MyResult::Ok(v.into()),
            MyResult::Err(e) => MyResult::Err(e.into()),
        }
    }
}

impl<T1, T2> From<MyOption<T1>> for MyOption<T2>
    where Pair<MyOption<T1>, MyOption<T2>>: NotSame,
          T2: From<T1>,
{
    fn from(r: MyOption<T1>) -> Self {
        match r {
            MyOption::Some(v) => MyOption::Some(v.into()),
            MyOption::None => MyOption::None,
        }
    }
}

fn main() {
    let option: MyOption<u64> = MyOption::Some(1u32).into();
    let result: MyResult<(), Box<Error>> = MyResult::Err("test").into();
}

#17
use std::env;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;

fn read_utf8_file<P: AsRef<Path>>(path: P) -> io::Result<String> {
    let mut f = try!(File::open(path));
    let mut s = String::new();
    try!(f.read_to_string(&mut s));
    Ok(s)
}

fn main() {
    let file_name = match env::args_os().nth(1) {
        Some(n) => {
            match n.into_string() {
                Ok(n) => n,
                Err(_) => {
                    println!("File name is not valid UTF-8.");
                    return;
                }
            }
        }
        None => {
            let exe_opt = env::current_exe().ok();
            let exe = exe_opt.and_then(|p| p.file_name().map(|p| p.to_string_lossy().into_owned()));
            println!("Usage: {} <file>", exe.unwrap_or("<binary>".into()));
            return;
        }
    };
    let content = match read_utf8_file(&file_name) {
        Ok(c) => c,
        Err(e) => {
            println!("Error: {}", e);
            return;
        }
    };
    println!("{}", content);
}

I tried what you suggested and it seems easy enough, though that’s likely because there’s a read_to_string method on Read.

The unfortunate thing is how much code is required to have the main function not panic, while providing fairly standard error messages.

If you don’t use read_to_string, then you get this without doing conversions between errors (which is how read_to_string is implemented:

fn read_utf8_file<P: AsRef<Path>>(path: P) -> io::Result<String> {
    let mut f = try!(File::open(path));
    let mut v = Vec::new();
    try!(f.read_to_end(&mut v));
    match String::from_utf8(v) {
        Ok(s) => Ok(s),
        Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "file did not contain valid UTF-8")),
    }
}

If you want to use try! instead of a match, then you get:

use std::env;
use std::error;
use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use std::string::FromUtf8Error;

#[derive(Debug)]
enum Utf8FileError {
    IoError(io::Error),
    Utf8Error(FromUtf8Error),
}

impl fmt::Display for Utf8FileError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            &Utf8FileError::IoError(ref e) => e.fmt(f),
            &Utf8FileError::Utf8Error(ref e) => e.fmt(f),
        }
    }
}

impl error::Error for Utf8FileError {
    fn description(&self) -> &str {
        match self {
            &Utf8FileError::IoError(ref e) => e.description(),
            &Utf8FileError::Utf8Error(ref e) => e.description(),
        }
    }
    fn cause(&self) -> Option<&error::Error> {
        match self {
            &Utf8FileError::IoError(ref e) => e.cause(),
            &Utf8FileError::Utf8Error(ref e) => e.cause(),
        }
    }
}

impl From<io::Error> for Utf8FileError {
    fn from(e: io::Error) -> Self {
        Utf8FileError::IoError(e)
    }
}

impl From<FromUtf8Error> for Utf8FileError {
    fn from(e: FromUtf8Error) -> Self {
        Utf8FileError::Utf8Error(e)
    }
}

fn read_utf8_file<P: AsRef<Path>>(path: P) -> Result<String, Utf8FileError> {
    let mut f = try!(File::open(path));
    let mut v = Vec::new();
    try!(f.read_to_end(&mut v));
    Ok(try!(String::from_utf8(v)))
}

Which, I agree is a bit much for a newbie. You don’t have to impl Error in this case, but if you’re going to go this far with the From conversions, might as well do Error too.


#18

My example wasn’t great since I forgot that you can read to an UTF8 string directly. Basically any case where you have two separate errors and want to handle both should do just as well.

Yes, strictly you don’t have to implement StdError, but then why have that trait in the first place if people aren’t going to implement it? It also means that the error can’t be used with Box<StdError>.

It’s just a lot of learn and a lot to type for something which should be common and simple.


#19

I mostly just wanted to type out the code and see what it ends up looking like.

The experience wasn’t very newbie friendly, given that I’m just writing the most basic version of ‘cat’. At some point I was returning try!(...) instead of Ok(try!(...)) from the read_utf8_file function and the error message says that it expects type _, but got std::string::String instead. I ended up looking up the definition of try! and changing it to a match manually before I realized that I was missing the Ok() around the try!.


#20

Another pain point similar to this one is when generic code is throwing errors defined via associated types. E.g. this:

trait Perform {
  type Error;
}

enum CompoundError<T> where T: Perform {
  PerformError(T::Error),
  OtherError(u32)
}

impl<T> From<T::Error> for CompoundError<T> where T: Perform {
  fn from(value: T::Error) -> Self {
    CompoundError::PerformError(value)
  }
}

This doesn’t compile due to coherence issues. I can’t see any other way to use From with generic types, other than this, unless I’m missing something. And this is a pretty annoying limitation, since many libraries (e.g. serde) have these types of generic error types. I don’t think specialization will solve this problem either, given that a type might implement several traits which could cause it to map to different From implementation simultaneously.

I realize that solving this problem is really hard, but it’s another example of where error conversion with From falls short in today’s Rust.

EDIT: playground link https://is.gd/ZEhsNf