Custom error struct with location

Hi,

I want to create custom error struct with std::panic::Location field.
Here is example:

use std::error::Error;
use std::fmt::{Display, Formatter};
use std::panic::Location;

#[derive(Debug)]
pub struct AnyError<T: Error> {
    pub error: T,
    location: &'static Location<'static>,
}

impl<T: Error> Error for AnyError<T> {}

impl<T: Error> From<T> for AnyError<T> {
    #[track_caller]
    fn from(err: T) -> Self {
        AnyError { error: err, location: Location::caller() }
    }
}

impl<T: Error> Display for AnyError<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error: {} - {}", self.error, self.location)
    }
}

This code works fine with only one error type. For example:

fn test_error() -> Result<(), AnyError<io::Error>> {
    let _ = std::fs::read_to_string("file_not_found.json")?;
    Ok(())
}

What I need to add/change for using AnyError<Box<dyn Error>>?

fn test_error() -> Result<(), AnyError<Box<dyn Error>>> {
    let _ = std::fs::read_to_string("file_not_found.json")?;
    let _: u32 = "1".parse()?;
    Ok(())
}

you cannot make it work both for concrete inner error type E: Error and erased Box<dyn Error + 'static>, there will be conflicts between the blanket implementation and the specialized implementation for Box<dyn Error + 'static>

if you want to keep the implementation for the parameterized inner type, you have to use something else to make the erased type work. e.g. an explicit extension trait to coerce to Box<dyn Error>. like this:

trait ResultExt<T> {
    fn box_error(self) -> Result<T, Box<dyn Error>>;
}
impl<T, E: Error> ResultExt<T> for Result<T, E> {
    //...
}

fn test_error() -> Result<(), AnyError<Box<dyn Error>>> {
    let _ = std::fs::read_to_string("file_not_found.json").box_error()?;
    let _: u32 = "1".parse().box_error()?;
    Ok(())
}

Implementation must be something like that?

impl<T, E: Error + 'static> BoxedError<T> for Result<T, E> {
    fn boxed_error(self) -> Result<T, Box<dyn Error>> {
        self.map_err(|e| Box::from(e))
    }
}

p.s. sorry, I just start learn Rust :slight_smile:

sorry I don't get what's your question. if you are referring to the trait name or type signature, no, it's just an example what you can do, no necessarily a requirement.

in your original example code, the wrapper type AnyError is parameterized on the inner type, this is all well and good, but later you also want to also use it as a type-erasure fixture. but the question mark operator uses From::from to do the conversion.

the problem is, AnyError<T> implemented From<T>, apply monomorphization (i.e. substitute T with Box<dyn Error>), you get something equivalent to (monomorphization:

impl From Box<dyn Error> AnyError<Box<dyn Error>> {
    //...
}

this means, when using question mark operator to short circuit returning Result<T, AnyError<Box<dyn Error>>>, the only type satisfying the From bounds is Result<T, Box<dyn Error>>.

you can solve the problem in too approach, depending on what trade off you are willing to make.

the first one, is what I provided in my previous comment, that is, you explicitly convert Result<T, E> into Result<T, Box<dyn Error>>, then the question mark operator convert it to Result<T, AnyError<Box<dyn Error>>>.

you can do the conversion however you like. for example, map_err() can be used with an regular function or lambda,

// a conversion function
fn box_error<E: Error>(e: E) -> Box<dyn Error> { Box::new(e) }

// use a function
let _ = std::fs::read_to_string("file_not_found.json").map_err(box_error)?;
// use a lambda, notice the verbose and uncommon syntax
let _: u32 = "1".parse().map_err(|e| Box::new(e) as Box<dyn Error>)?;

using an extension trait just make the code more concise and easier to write.

option two, is to change the blanket From trait implementation so you only have:

impl From<E: Error> for AnyError<Box<dyn Error>> {
    //...
}

then the question mark operator works for every type E: Error, for example, std::io::Error and std::num::ParseIntError, so your example code will work:

let _ = std::fs::read_to_string("file_not_found.json")?;
let _: u32 = "1".parse()?;

but you will have to give up the blanket implementation for the generic AnyError<E>, i.e. the following impl blocks must be removed:

// give up this
//impl<E: Error> Error for AnyError<E> {}

// and this
//impl<E: Error> From<E> for AnyError<E> {}

// in order to have this:
impl<T: Error + 'static> From<T> for AnyError<Box<dyn Error>> {
    #[track_caller]
    fn from(err: T) -> Self {
        AnyError { error: Box::new(err), location: Location::caller() }
    }
}

because they conflicts for the type AnyError<Box<dyn Error>>.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.