Error handling


#1

Not sure if this has been mentioned before, but I’ve currently got a lot of code like the following:

fn my_func() -> Result<Obj, MyError> {
    let file = File::open("some/path")
        .context(ErrorReadingConfig)?
        .read_to_end()
        .context(ErrorReadingConfig)?;

    let mut d = Deserializer::from_bytes(&file);
    let val = T::deserialize(&mut d)
        .context(ErrorReadingConfig)?;
    d.end()
        .context(ErrorReadingConfig)?;
    Ok(val)
}

Maybe it would be nice to be able to do the following:

fn my_func() -> Result<Obj, MyError> {
    try {
        let file = File::open("some/path")?
            .read_to_end()?;

        let mut d = Deserializer::from_bytes(&file);
        let val = T::deserialize(&mut d)?;
        d.end()?;
        val
    }.with_context(ErrorReadingConfig)
}

but I don’t know if that’s too much magic.

Was wondering what the current thinking from the powers that be is?


#2

What library is adding in Result::context? It seems like this should be possible to handle via do catch relatively trivially (depending on how that context method works anyway):

fn my_func() -> Result<Obj, MyError> {
    let res: Result<Obj, MyError> = do catch {
        let file = File::open("some/path")?
            .read_to_end()?;

        let mut d = Deserializer::from_bytes(&file);
        let val = T::deserialize(&mut d)?;
        d.end()?;
        val
    };
    res.context(ErrorReadingConfig)
}

#3

The problem is that each error returned is a different type, but each of these different types has a ‘context’ function, which returns the same type for all the input types (basically makes it a trait object and wraps it in a new type with indirection). So this would have to be

let res: Result<Obj, TraitObjectButDontBoxMeYet> ...

#4

Note: do catch will likely be renamed to try { .. }.


#5

This sounds like a job for plain old generics. I’ve encountered a similar situation before, and it’s very easily solvable by extending Result generically and requiring (and implementing) a trait bound on each of the error types. Note that if many types implement a method with the same signature, it should have been a trait anyway. If even the implementation of the .context() method is uniform, then it’s possible that even a default implementation suffices. Something along the lines of:

struct ErrorWithContext(Box<Error>); // or something

trait ContextualError: Error + Sized + 'static {
    fn context(self) -> ErrorWithContext {
        ErrorWithContext(Box::new(self))
    }
}

struct FooError;
impl ContextualError for FooError { … }

struct BarError;
impl ContextualError for BarError { … }

trait ContextualResult<T>: Sized {
    fn context(self) -> Result<T, ErrorWithContext>;
}

impl<T, E: ContextualError> ContextualResult<T> for Result<T, E> {
    fn context(self) -> Result<T, ErrorWithContext> {
        self.map_err(E::context)
    }
}

For the sake of clarity, context here doesn’t take arguments other than self, although the code is easily modified to accommodate additional arguments.


#6

Thanks for taking the time to show this.