Syntax for returning early with an error

You're totally right. I misread the situation (I assumed that this feature has already been discussed before, and that there already was a proposal for it, because the lang team had discussed it in April 2020, and the new Try trait RFC mentions it as well).

I'm not sure if I want to write an RFC for this, because I'm not very good at it. And even if someone else does, the RFC will probably not be accepted in time to reserve the keyword in the 2021 edition. That means the feature has to wait another 3 years, but the upside is that this gives us more than enough time to flesh out the details and bikeshed the name :wink:

I would however appreciate a bit more feedback, both positive and negative, because this forum doesn't have a downvote button, which makes it difficult to get an impression of how people feel about this. Is it worthwhile to pursue this idea, or am I just wasting my time?

The problem of this is of cause, it also implied "something is wrong" when early returning can mean a good result (especially when you are running a search algorithm and found something, rather than having to say "not found").

Having that said, I think the keyword that we currently already have is the break. We only need to find a way to define its scope better so it is not breaking anything.

2 Likes

I think there's plenty of interest in the overall idea, and figuring out how it should work is definitely valuable. Especially since, as was brought up, my RFC means there's at least two different ways the semantics could work.

Keyword conversations are annoying, but I'm liking a bunch of the conversation here about how it should be structured. Things like whether yeet foo or yeet Err(foo) or whatever, as well as conversations about whether it includes conversions -- does yeet 4 work for Result<_, u8>, for example?

There might be some value in not adding a keyword at all and just keeping a yeet(...)?-style solution. Like in async blocks where every break point is marked with .await or in loops where every break is marked with break, this would keep a syntactically very uniform situation where in a try block, every (potential) "failure" path is marked with ?.

I mean, Rust is in some ways a "functional" language, so a function-based solution should not be underrated. For comparison, something like drop is a standalone function as-well, not a keyword. And the Result return type is just an enum type, not some kind of throws keyword in the function signature.

This doesn't do anything for the naming debate, of course.

5 Likes

What's the motivation for this? Why isn't return good enough for returning? Why isn't ? good enough for returning early with an error? Why would we need yet another control flow keyword?

There's already plenty of syntactic sugar around Try types. We don't need more of that.

(Please spare me the "it's shorter to write" arguments, while we are at it. Neither return nor ? warrant further shortening.)

1 Like

return doesn't work for try {} blocks because return exits the function. That said, I actually do like the idea of writing it as yeet(x)?; rather than yeet x;, and in that case ? is the only way that you bubble a try break.

try {} should probably also be a valid break target, with the same success wrapping applied as on the tail expression. (Grand evil plan: break works to return a value from the function as well, and return is just sugar for break 'fn :grinning_face_with_smiling_eyes:)

6 Likes

Alright, but this is not an early return proposal then.

Although this still doesn't explain why we need a new keyword. What would be the advantage compared to constructing the type directly? There's already Try::from_error() even in a generic context, when Err can't be used.

1 Like

I too have thought of your grand evil plan. I guess a problem for yeet(x)? is how it is supposed to know that you want a "success" or a "failure" but that would still be a problem with a keyword.

And I like the uniformity of having only ? be the bubble siggel.

One reason I would like a feature like this is that Err(..)? triggers a clippy warning, because it is not idiomatic. However, return Err(..) doesn't work in try blocks, and I would like to have an idiomatic syntax, and that syntax should be the same within try blocks. And since try blocks do Ok-wrapping, it seems reasonable to also have a syntax that does Err-wrapping. (except that it has to work with any Try type)

Furthermore, the bail! macros implemented by many error-handling crates prove that there is demand for a more ergonomic syntax. Unfortunately, these bail! macros return from the function, but I think it's usually desired to only exit the enclosing try block (i.e., do the same as ?). This has the potential for confusing bugs once try blocks get stabilized. I think a special syntax that supersedes bail! macros and works as most people would expect within try blocks would help.

Furthermore, this syntax could be used with any error-handling library (and even when no such library is used), which would help unifying the error handling ecosystem and make it easier to switch from one error-handling library to another.

If that syntax is implemented as a function instead of a keyword, I'm happy with that. That function could even be called bail :slight_smile:

First, Try::from_error(..)? is arguably not idiomatic, and second, the Try trait is unstable and most likely won't be stabilized in its current form. Currently the best candidate for stabilization is the try_trait_v2 RFC (mentioned above), which doesn't have a Try::from_error method.

2 Likes

That could be changed, though, and even if it isn't, you can just disable the Clippy warning.

Why/how so? Actually, I'd argue that it should be accepted as-is. If the argument against it is that it hides the return, then that's inconsistent because a new keyword (either combined with ? or without it) would also have that problem. And it's not even a problem per se — ? is so widely-known and idiomatic that it should definitely be recognized as a control flow operator.

I'm sorry but I just don't buy that a single Err-wrap or method call is not ergonomic enough. One must be willing to type that much.

These macros could just be rewritten using the existing Err(…)? syntax to not return unconditionally.

Then why should we start RFC'ing yet another piece of special syntax, instead of moving forward with an already-existing effort? That would be getting priorities wrong.

2 Likes

What exactly are you guys discussing here? “Try::from_error(..)?does not even work.


What are you referring to with “already-existing effort”? If it’s the “try_trait_v2” RFC, well... that one explicitly does not include a yeet e-style keyword while staying compatible with it. So “start[ing] RFC’ing yet another piece of special syntax” literally is “building on this already-existing effort” in this case.

2 Likes

It would seem to me that these are 2 orthogonal concerns: exiting a scope (return, break, ?, .await, later on perhaps yield), and value wrapping.

Rather than adding another keyword that conflates those two, if any a separate syntax of sorts just for the (Ok/Err/whatever) wrapping would strike me as more generally useful.

1 Like

There’s also the possibility to use a unit struct with custom FnOnce [/Fn] implementations to go even further, to replicate the yeet e; vs yeet; distinction I discussed above, allowing for things like

fn foo() -> Result<i32, bool> {
    yeet(false)?;
}
fn bar() -> Option<i32> {
    yeet()?;
}

Here’s a proof-of-concept implementation using try_trait_v1.

Edit: Actually, a similar aproach also allows for the option

fn foo() -> Result<i32, bool> {
    yeet(false)?;
}
fn bar() -> Option<i32> {
    yeet?;
}

(playground)

1 Like

Centril explored these ideas. I doubt a new keyword is warranted unless you take into consideration future goals beyond not typing Err(...)?. But not everyone is on board with the grand evil plan. If the only motivation large enough for a language addition is a grand evil plan you don't agree with, you tend to oppose said addition, even if you don't have a problem with the smaller change per se.

I'm personally against invisible wrapping based on context. Separating these concerns and making value wrapping ergonomic without being invisible appeals to me.

It's worth noting that it's a decided fact that try { x } will perform "Ok wrapping" of x. This one small detail was decided through a full T-lang FCP and signoff, and is not subject to change. Any other potential design should keep this in mind.

(Thus, if break can use a try block as a break-with-value target, whether the landing is automatic or explicit, it should probably behave the same as a tail value, thus wrapping in Try::from_ok.)

3 Likes

If the value of a break expression is Ok-wrapped, that means however that it's impossible to break with an error value, right?

I am aware, but thank you, that is something that should be highlighted in this thread. Here's the signoff; I recommend reading @josh's summary at the top.

2 Likes

That's what yeet is for :grinning_face_with_smiling_eyes:

(And yes, this means breaking with a pre-wrapped variable that could programatically be either is awkward. There's always tradeoffs.)

Is it so awkward? Well-behaved Try implementations should have trivial roundtrip conversions that don’t change anything, so with an eventual break for try {} blocks, the correct way to return a pre-wrapped variable v would be by writing “break v?;”.

5 Likes

Ahh, This gives me an idea.

In regards to the non-default wrapping gramma, if break v always mean Ok(v) for a Result typed value, break? v can mean Err(v) for the same Result type. This can work for all other types that have two possible return variants, and does not imply a meaning of "fail" or "error".