Thank you for this summary, it really helps organize the many things going on here!
To me, a major strength of Rust is a very simple, core feature: I can use types as footholds in reasoning about my programs. Matching on enums (reasoning by cases!) is my favorite example of this.
I found myself struck by this line:
I think it could be very useful to think of error handling as “exceptions that one must manually propagate at each level” and then, once you’ve learned about enums etc, understand how that works under the hood.
This seems out-of-order: I would teach a new Rust user about enums immediately, long before the (important!) practical matter of using Result idiomatically.
Here is a data point from another language family: in the past, before I had a need to use them, I found myself a bit confused by async function and function* in JavaScript. The first time I went to use those features, it was while writing TypeScript. It was there, seeing the “external” return type written out, that made everything really click for me.
Now, conflating a T and a Promise<T>, or a T and a Result<T, E> can be either helpful or harmful. In the JS case, after seeing the concrete, true type of an async function, I found it easy to go back elsewhere (e.g. in a plain JavaScript codebase) and conflate the types in a way that was helpful: I always felt like I was on solid ground, because I had the true type in my head.
Similarly, when I have a Rust function that returns a Result<T, E>, I really do mostly think of its return type as a Result<T, E>, not a T– even when I locally want to conflate the two. Because of this, I have found it extremely easy to write my own error handling helpers, and think about how they might be composed.
The ? operator is really great, in that framing, because it lets me “unwrap or bail” explicitly. Note that I think of it as “bail”, as in early return, and not “throw an exception, kinda”. I know exactly where I’m bailing to: historically, the end of the function, or in the case of a catch block, another explicit, function-local place.
When I write error-handling in Rust, I am not thinking of ? as marking a “happy path” (like I might with, say, a hypothetical do notation): I am always thinking of as a shorthand for a particular (tedious, verbose) type of match-and-conditional-branch. In that sense, catch blocks are nice without the exception analogy, because I can think of it as “break from this block” (as if I were in a loop), vs. “return from this function”.
I happily use TypeScript’s try/catch for promises, and I like the idea of throw. But, unlike many languages, Rust has a sturdy foundation of real enums and pattern matching. I think that should be the framing that we emphasize and prioritize when teaching and improving error handling, whatever the syntax.