Description
openedon Jun 21, 2022
Proposal
Add new API try_downcast_inner
to std::io::Error
:
impl std::io::Error {
fn try_downcast_inner<E: Error + Send + Sync + 'static>(self) -> Result<Box<E>, Self>;
}
Problem statement
Existing APIs requires two separate calls to obtain the raw os error or the inner error and they both return Option
.
There's no way to avoid unwrap
/expect
if we want to cover every possible corner case here, e.g., both of them somehow returns None
.
This is impossible without expect
/unwrap
because std::io::Error::into_inner
takes the error by value and returns Option<Box<dyn Error + Send + Sync>>
instead of Result<Box<dyn Error + Send + Sync>, Self>
.
Currently, we would have to do this to downcast the inner error:
(Adapted from cargo-bins/cargo-binstall#180)
#[derive(Debug)]
#[derive(thiserror::Error)]
enum E {
#[error(transparent)]
Io(std::io::Error),
#[error("...")]
SomeOtherVariant,
}
impl From<std::io::Error> for E {
fn from(err: std::io::Error) -> E {
if err.get_ref().is_some() {
let kind = err.kind();
let inner = err
.into_inner()
.expect("err.get_ref() returns Some, so err.into_inner() should also return Some");
inner
.downcast()
.map(|b| *b)
.unwrap_or_else(|err| E::Io(std::io::Error::new(kind, err)))
} else {
E::Io(err)
}
}
}
This has only 1 expect
/unwrap
, but it uses std::io::Error::new
, which uses Box::new
internally.
Another way to implement it would be:
impl From<std::io::Error> for E {
fn from(err: std::io::Error) -> E {
let is_E = err
.get_ref()
.map(|e| e.is::<E>())
.unwrap_or_default();
if is_E {
let inner = err
.into_inner()
.expect("err.get_ref() returns Some, so err.into_inner() should also return Some");
inner
.downcast()
.map(|b| *b)
.expect("e.is::<E>() returns true, so this must succeed")
} else {
E::Io(err)
}
}
}
This saves the call to std::io::Error::new
, but adds another unwrap
/expect
.
Thus, I propose to add another API to simplify the downcasting while avoiding unwrap
/expect
, which is std::io::Error::try_downcast_inner
.
It returns Result<Box<E>, Self>
, and when the downcast fails, it simply return the original std::io::Error
.
Motivation, use-cases
This makes downcasting much easier:
#[derive(Debug)]
#[derive(thiserror::Error)]
enum E {
#[error(transparent)]
Io(std::io::Error),
#[error("...")]
SomeOtherVariant,
}
impl From<std::io::Error> for E {
fn from(err: std::io::Error) -> E {
err.try_downcast_inner::<E>()
.map(|b| *b)
.unwrap_or_else(E::Io)
}
}
Solution sketches
The first solution I came to mind with is:
impl std::io::Error {
fn into_underlying_err(self) -> Cause;
fn from_underlying_err(cause: Cause) -> Self;
}
#[non_exhaustive]
enum Cause {
RawOsError(i32),
InnerError(Box<dyn Error + Send + Sync>),
}
Then I realized that this is too complex and exposes too much details of std::io::Error
.
The second solution:
impl std::io::Error {
fn into_inner2(self) -> Result<Box<dyn Error + Send + Sync>, Self>
}
The problem with this solution is that it creates confusion by introducing a new function with name very similar to existing one.