2

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.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
Renato
  • 12,940
  • 3
  • 54
  • 85
  • 1
    I know that isn't exactly what you're asking. But in short, it's because `?` is sugar, which includes converting the error using `From`. If just doing `return`ing alone did that, then that would be kinda _scary_. – vallentin Feb 23 '21 at 18:47
  • The equivalent match expression doesn't seem to call `into()` or whatever to invoke the `From` though? Does it say it does that somewhere? – Renato Feb 23 '21 at 18:50
  • If anything, that question made me more confused than before @vallentin. According to that answer, the `?` shouldn't work the way it does in my example. – Renato Feb 23 '21 at 18:53
  • At the bottom of the answer check "See also" specifically [this question](https://stackoverflow.com/questions/40545332/is-the-question-mark-operator-equivalent-to-the-try-macro). `?` is equivalent to `try!`, if you check the source for [`try!`](https://github.com/rust-lang/rust/blob/019610754363d1d92a8d0f364d2c0909d6f53dfd/library/core/src/macros/mod.rs#L300-L309), then you can see it uses `From::from(err)` – vallentin Feb 23 '21 at 18:55
  • 1
    The [Rust Book](https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html) doesn't seem to mention anything about `From:from(err)` and this other answer you link to doesn't either. It would be nice to have a "proper" answer showing what `try!` actually does (not shown in that answer). Only the RFC mentioned in that answer seems to show usage of `e.into()` but that was a design proposal (which looks different from current Rust). Very unsatisfactory situation IMO. – Renato Feb 23 '21 at 19:04
  • 1
    @vallentin sure but how indirect an answer has to be before you start agreeing with me it's not addressing my question (and that RFC is NOT the same as the current Rust implementation)? The [try!](https://doc.rust-lang.org/std/macro.try.html) docs mention that _try! performs conversion using From_ in case of error, and that the `?` operator "replaces" it... – Renato Feb 23 '21 at 19:09

1 Answers1

3

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 match expression from Listing 9-6 does and what the ? operator does: error values that have the ? operator called on them go through the from function, defined in the From trait in the standard library, which is used to convert errors from one type into another. When the ? operator calls the from function, 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 the from function 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 the std::convert::Into trait for this purpose (which has a blanket impl forwarding from From). [...]

Exception type upcasting - RFC 243

See also:

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.

vallentin
  • 23,478
  • 6
  • 59
  • 81
  • For completeness sake, you could get around the extra `Ok(())` by using `other_error().map_err(From::from)`, which essentially does the same conversion thing as in the match statement. But that is not really less verbose. – Johann150 Feb 23 '21 at 19:46
  • @Johann150 Indeed, but to be even more clear. That trick _only_ works using `From::from` if the compiler can infer the correct type, which you can quickly run into issues with. In that case it might be better to use `.map_err(MyError::from)` instead. (Anyways, thanks, I have updated the answer to include that) – vallentin Feb 23 '21 at 20:45