std::io:Error - how to get the "inner" error out as the concrete type, but only if it matches the expected type?

Supposed I have a custom error, that sometimes "wraps" an std::io::Error:

use std::io::{ErrorKind, Error as IoError};

pub enum MyError {
    Cancelled,
    TimedOut,
    Incomplete,
    Failed(IoError)
}

Now, I want to be able to convert between MyError and IoError, in both directions:

impl From<MyError> for IoError {
    fn from(value: MyError) -> Self {
        match value {
            MyError::Failed(error) => error,
            other => IoError::new(ErrorKind::Other, other),
        }
    }
}

impl From<IoError> for MyError {
    fn from(value: IoError) -> Self {
        match get_inner(value) {
            Ok(inner_error) => inner_error,
            Err(other) => MyError::Failed(other),
        }
    }
}

fn get_inner<T>(error: IoError) -> Result<T, IoError>
where
    T: Error + 'static,
{
    match error.get_ref().and_then(|err| err.downcast_ref::<T>()) {
        Some(_) => Ok(*error.into_inner().unwrap().downcast::<T>().unwrap()),
        None => Err(error),
    }
}

My problem is: How do I get the concrete MyError out of IoError, in case that the IoError contains a MyError as its "inner" error – but otherwise retain the IoError so that it can be wrapped?

Is there a "better" way to implement get_inner(), or completely get rid of it?

Thing is, get_ref() + downcast_ref() gives me &MyError when that is possible, but does not allow me to return the MyError as an owned value. Meanwhile, into_inner() consumes the original IoError, so in case we could not convert to MyError, the original IoError is gone :weary:

Workaround is to combine both approaches.

But, having to do the downcast twice – the first time just for test – seems redundant :thinking:

Basically, I'd need something that gives me the "inner" error as its concrete type, if and only if the IoError does contain an "inner" error of the desired type. Otherwise, just give me back the IoError as-is. Unfortunately, into_inner() consumes to original IoError, so there is no way back...

Thank you!

This sounds like std::io::Error::downcast, no? This is currently still unstable though, so only available on nightly:

#![feature(io_error_downcast)]

use std::io::{ErrorKind, Error as IoError};
use std::error::Error;
use std::fmt::{self, Display};

#[derive(Debug)]
pub enum MyError {
    Cancelled,
    TimedOut,
    Incomplete,
    Failed(IoError)
}

impl Error for MyError {}

impl Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl From<MyError> for IoError {
    fn from(value: MyError) -> Self {
        match value {
            MyError::Failed(error) => error,
            other => IoError::new(ErrorKind::Other, other),
        }
    }
}

impl From<IoError> for MyError {
    fn from(value: IoError) -> Self {
        match get_inner(value) {
            Ok(inner_error) => inner_error,
            Err(other) => MyError::Failed(other),
        }
    }
}

fn get_inner<T>(error: IoError) -> Result<T, IoError>
where
    T: Error + Send + Sync + 'static,
{
    error.downcast::<T>().map(|e| *e)
}

Playground.

1 Like

If only downcast() wasn't "unstable" :rofl: :frowning_face:

Is there any way to emulate a function like downcast() with current "stable" Rust?

I think this is the current approach that the new downcast should fix:

Existing APIs requires two separate calls to obtain the raw os error or the inner error and they both return Option.

Thus, users would have to first call Error::get_ref to check that we indeed has an inner error and checked that it is the type we expected.

From the tracking issue.

If you look at the implementation of downcast, you'll see that it requires access to the private repr field of std::io::Error, so I don't think you can emulate it any other way.

1 Like

Okay, thanks!

Just to be sure: If I use any "unstable" features in my crate, other users/projects won't be able to use my crate, unless they switch to the "nightly" tool-chain as well, right?

Yes, see here:

Thanks for confirming. So I probably can't use io_error_downcast feature for now.

At least my code could be simplified a bit by using is:

pub fn try_downcast_error<T>(error: IoError) -> Result<T, IoError>
where
    T: Error + Send + Sync + 'static,
{
    match error.get_ref().map(|inner| inner.is::<T>()) {
        Some(true) => Ok(*error.into_inner().unwrap().downcast::<T>().unwrap()),
        _ => Err(error),
    }
}
1 Like

new is a way back, if you know into_inner will give you Some. In other words, you'd still have to probe with get_ref, but you don't have to downcast twice.

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.