Given the following two error types and functions to illustrate their usage (Rust Playground link):
#[derive(std::fmt::Debug)]
struct MyError;
#[derive(std::fmt::Debug)]
struct OtherError;
impl std::error::Error for MyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "MyError")
}
}
impl std::fmt::Display for OtherError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "OtherError")
}
}
impl std::error::Error for OtherError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl From<OtherError> for MyError {
fn from(_: OtherError) -> Self {
MyError {}
}
}
fn my_error() -> Result<(), MyError> { Ok(()) }
fn other_error() -> Result<(), OtherError> { Ok(()) }
If I am in a function that returns Result with MyError as its Error type, I can call both functions returning MyError and OtherError because there's a From converting between them.
However, I cannot simply return the Result for the "other" type, I need to use ? followed by Ok(()) instead. This looks inconsistent to me.
For example, this works fine:
fn main() -> Result<(), MyError> {
my_error()
}
This also does:
fn main() -> Result<(), MyError> {
my_error()?;
other_error()?;
my_error()
}
But this fails:
fn main() -> Result<(), MyError> {
my_error()?;
other_error()
}
Error:
error[E0308]: mismatched types
--> src/main.rs:43:5
|
41 | fn main() -> Result<(), MyError> {
| ------------------- expected `std::result::Result<(), MyError>` because of return type
42 | my_error()?;
43 | other_error()
| ^^^^^^^^^^^^^ expected struct `MyError`, found struct `OtherError`
|
= note: expected enum `std::result::Result<_, MyError>`
found enum `std::result::Result<_, OtherError>`
Why is that?
This makes some of my code more verbose, as I found out I need to do this to get it to work:
fn main() -> Result<(), MyError> {
my_error()?;
other_error()?;
Ok(())
}
Is this the only solution? I am more interested in understanding the reason it works this way, but if I am doing something silly feel free to point out what could be done better.
The ? operator is equivalent to the try! macro, which a simplified version is as follows:
macro_rules! r#try {
($expr:expr $(,)?) => {
match $expr {
Ok(val) => val,
Err(err) => {
return Err(From::from(err));
}
}
};
}
You can find a reference for this in the book on the Recoverable Errors with Result page, in the A Shortcut for Propagating Errors: the ? Operator section.
There is a difference between what the
matchexpression from Listing 9-6 does and what the?operator does: error values that have the?operator called on them go through thefromfunction, defined in theFromtrait in the standard library, which is used to convert errors from one type into another. When the?operator calls thefromfunction, the error type received is converted into the error type defined in the return type of the current function. This is useful when a function returns one error type to represent all the ways a function might fail, even if parts might fail for many different reasons. As long as each error type implements thefromfunction to define how to convert itself to the returned error type, the?operator takes care of the conversion automatically.– A Shortcut for Propagating Errors: the ? Operator - The Rust Programming Language
(emphasis mine)
You can also find a reference for this in RFC 243 where you can find the information in the Exception type upcasting section.
The
?operator should therefore perform such an implicit conversion, in the nature of a subtype-to-supertype coercion. The present RFC uses thestd::convert::Intotrait for this purpose (which has a blanketimplforwarding fromFrom). [...]– Exception type upcasting - RFC 243
See also:
Error types in Rust?The Other uses of ? page in Rust By Example also mentions the implicit conversion behavior. It also provides an example of creating custom error types.
So in conclusion, the following:
fn main() -> Result<(), MyError> {
my_error()?;
other_error()?;
Ok(())
}
Is essentially the equivalent to:
fn main() -> Result<(), MyError> {
match my_error() {
Ok(_) => {}
Err(err) => return Err(From::from(err));
}
match other_error() {
Ok(_) => {}
Err(err) => return Err(From::from(err));
}
Ok(())
}
In the case of returning other_error() and having it implicitly convert errors, then that would be kinda scary. As if e.g. the return type was modified, and return ... implicitly just Into, then it could introduce issues that aren't immediately obvious.
If you want to avoid the additional Ok(()), then you can use map_err(), i.e. .map_err(From::from). However, if it isn't returned immediately then you can easily run into cases where the compiler cannot infer the correct type.
In those cases you can use a more explicit form, i.e. .map_err::<MyError, _>(From::from), or just .map_err(MyError::from).
fn main() -> Result<(), MyError> {
my_error()?;
// other_error().map_err(From::from)
// other_error().map_err::<MyError, _>(From::from)?;
other_error().map_err(MyError::from)
}
If your MyError was an enum, which had a MyError::OtherError variant, then you could even just do .map_err(MyError::OtherError).
This is because, not only does constructing an enum look like a function call, it is actually implemented as functions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With