Is the current Error trait optimal?


#1

I think we need to have a discussion on the std::error::Error trait (again?) Here is the definition for your reference…

pub trait Error: Debug + Display + Reflect {
    fn description(&self) -> &str;
    fn cause(&self) -> Option<&Error> { ... }
}

As the Error trait stands now it is difficult for someone to write an implementation for the description method without preallocating a String or using an str with a 'static lifetime. For example you cannot use format!() to return an informative error string if you want to go beyond static strings. Look at the following implementation to understand where I am coming from…

pub enum MyError<'a> {
    ...
    ParseIntError {
        error: ParseIntError,
        iter: CharIndices<'a>,
        err_msg: String,
    },
}


impl<'a> Error for MyError<'a> {
    fn description(&self) -> &str {
        match *self {
            // Note: the error message cannot be constructed here since we have 
            // an immutable reference to Self. All we can do is simply pass on 
            // what we have here.
            MyError::ParseIntError { ref err_msg, .. } => err_msg,
            ...
        }
    }
}

impl<'a> From<(ParseIntError, CharIndices<'a>)> for MyError<'a> {
    fn from(err: (ParseIntError, CharIndices<'a>)) -> MyError<'a> {
        let error = err.0;
        let iter = err.1;

        // NOTE: The error message has to be allocated and constructed ahead 
        // of time. There is no guarantee that it will ever be used but we have 
        // to take a performance hit here anyway. This goes against Rust's pay 
        // as you go philosophy.
        let err_msg;

        if let Some((i, _)) = iter.clone().next() {
            err_msg = format!("{} @ {}", error.description(), i);
        } else {
            err_msg = format!("{} @ UNKOWN", error.description());
        }

        MyError::ParseIntError {
            error: error,
            iter: iter,
            err_msg: err_msg,
        }
    }
}

Playpen link to a more complete example.

IMO either of the following signatures will get rid of the preallocation / a priori construction problem…

    pub trait Error: Debug + Display + Reflect {
        fn description(&self, &mut String);
        fn cause(&self) -> Option<&Error> { ... }
    }

    pub trait Error: Debug + Display + Reflect {
        fn description(&mut self) -> &str;
        fn cause(&self) -> Option<&Error> { ... }
    }

I hope I am doing this the right way if not please do let me know.


#2

The Display supertrait bound is intended to be how one gets a fully-computed string, e.g. format!("{} @ {}", error, i). The description method is designed for a high-level overview of what went wrong.


#3

The Error trait is probably not optimal, but this is an intentional part of the design. Description is for a simple description (typically a fixed string literal is used) and Display is for the full information. Here’s a role model from libstd’s NulError if you want.


#4

Thanks guys. I think I understand the design now.