Rust's ? is not exception handling
People arriving from languages with exceptions read ? as a throw, and then expect
something up the stack to catch it. Nothing does. There is no catch, no unwinding, no handler search.
expr? expands to roughly this:
match expr {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
}
That is the whole feature. An early return, plus a From conversion on the error type.
Why that distinction pays
Every propagation point is visible in the source. A function that can fail says so in its return type, and every place a failure travels through is marked with a character you can grep for. There is no action at a distance, and no invisible control flow.
It also means the cost is a branch and a move, not a stack walk. Errors being cheap is why Rust
code returns Result for ordinary, expected failures instead of reserving it for
catastrophes.
The From conversion is the part worth learning
It is what lets a function returning Result<T, MyError> use ? on an
io::Error, provided impl From<io::Error> for MyError exists. That one
impl is the seam where a library's error taxonomy gets built, and thiserror exists
mostly to write those impls for you.
The corollary is that ? in a function returning Result<T, Box<dyn
Error>> accepts almost anything, which is convenient in a binary and a poor choice in a
library — you have erased the type your callers would need to match on.