I'm fairly new to Rust and working on a little personal project. I'm using reqwest and started learning about the Result type. After trial and error, I managed to get a function working which has the following signature:
I had to use Box<dyn Error> because of reqwest but I still wanted to be able to return my own type of Error. I solved this by returning
Err("Unknown page")?
at the end of the function.
However, I'm not sure why I have to use ? in the end. Another solution that worked was .into() which I also found confusing.
My question is: why do I need to use ? or .into(), how they work and is there a difference between them? As a secondary question: is there a better, more common way to return my own Error, compared to what I have done?
Before ? operator was introduced, Rust users use try! macro (deprecated in favor of ? operator) for the same purpose to handle result.
You can see its implementation to understand what ? does for Result<T, E>.
Essentially, res? (where res: Result<T, E>) is almost same as:
match res {
Ok(val) => val,
Err(err) => return Err(err.into()),
}
Now it is obvious that returning Err? and returning Err(err.into()) works the same way.
It would be better to use Err(e.into()) if possible (such as in your case), because ? implicitly introduces match and use of std::ops::Try.
They could be optimized away by rust compiler, but there are no reasons to make it unnecessarily complex.
Thank you @nop_thread that's very helpful! I think it's getting clearer for me but could you explain how .into() works as well? How come this
Err("Unknown page".into())
makes the compiler happy? I think the fact that into() doesn't accept any arguments seems like a bit of magic to me. I guess Rust somehow inspects the return type to determine what the destination type of .into() is?
For Box<dyn Error> (and its variants with + Send + Sync), there are From<&str> and From<String> implementations to convert strings into errors.
And v.into() returns From::from(v) by default.
So, things below happens:
Err("string".into()) is interpreted as Err(Into::<Box<dyn Error>>::into("string")).
Into::<Box<dyn Error>>::into("string") returns <Box<dyn Error> as From<&str>>::from("string").
<Box<dyn Error> as From<&str>>::from("string") returns Box<dyn Error> for you!
Now Err("stirng".into()) is evaluated to Result<_, Box<dyn Error>>.
It does accept an argument, in fact: the value it was called on ("Unknown page", in your case). This is so-called receiver parameter, implicitly passed into every method call. You could also do the exact same thing explicitly, by calling <&str as Into<Box<dyn Error>>>::into("Unknown page"), or in less verbose way - as a trait associated function - Into::into("Unknown page"); check this playground as a working example.
Of course, this is a part of the type inference. Rust knows exactly what value the function should return, and if you leave any unspecified types (as in call to .into()), it tries to find the one type which would satisfy both requirements - on the (receiver) argument type, which is &str, and on the return type, which is Box<dyn Error>. Since this type does exist and is unique, Rust uses it.