Syntax for returning early with an error

Which one can even argue is a good things -- it means that every possible "error propagation" point (if this is using result) is marked with a ?.

It's like my old argument (from 2017 :cold_sweat:) that the version with continue-wrapping

try {
    foo()?;
    bar()?;
    qux()?;
}

is better than the version without it

try {
    foo()?;
    bar()?;
    qux()
}

even though it's two characters longer, because it's more consistent in error-visibility and error-conversion and in how-to-add-another-call.

4 Likes

It does, in the very context (generics) where it was incorrectly claimed there's no alternative for non-Result types.

The current Try trait, which, as demonstrated above, is sufficient in a generic context, obviating the need for a new keyword.

Well, that's T::from_error(..)? and Err(..)? works just as well in this example (with the current Try trait).

One thing that's not clear to me is how yeet would work with streams. Would it be analogous to return, or to yield? And if one, wouldn't there be a need for an equivalent for the other - say, toss?

(starting to want a periodic table of keywords)

Because ? on the break path (which is not the path taken by break in a try block) does a return equivalent in any reasonable stream syntax, I think so should yeet. Personally in my experiments with fallible stream syntax I haven’t had a need for the syntax itself to support resuming after an error, that can be handled at a different level, just like a fn using ? doesn’t support resuming at the erroring point.

There is also the potential for the difference between a stream syntax and a try stream syntax, which could both be used to create a fallible stream but with different handling of errors.

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").

Isn't this keyword supposed to be for returning a Result::Err, implying that something went wrong? Or maybe I am misunderstanding.

I assume that this wants to be more general then that. Since Rust (imo) prefers providing the tools for the users implement on top of it.

The OP post states:

Essentially, this is syntactic sugar for Err(...)? , except that it should work for any type that implements Try , not just Result .

That is very specific to returning an error - when something goes wrong.

Try isn't limited to the early exit case being an error. As far as Try is concerned, it's just early exit versus continue, with no connotation of success or failure.

Consider the simple case of a linear search for a value. When you find it, you want to yeet[1] it back to the caller, but if you haven't found it, keep looking.

[1] placeholder syntax

2 Likes

Too bad I sort of want yeet. :stuck_out_tongue:

2 Likes

Maybe bounce or bubble as a keyword?

The problem is that there seems to be a contradiction in requirements here. "Try” itself implies error handling. Why else would one want to return early from a "try" unless the "try" was unsuccessful?

In my personal opinion, it would be bad English-wise to have the "yeat" operator inconsistent in terminology with the containing "try" block.

The try block is isomorphic to a "once" loop, and if we are to have a general construct than this would have been a more honest name for it. (This is hardly a new idea, I'm sure others have suggested this years ago).

The next logical step would then be to generalise the existing "early break from loop" syntax to an additional looping construct. This is a simple and obvious step:

let val = once {
    if cond {
        break 42;
    }
}

Of course, the most general form would also allow a labelled outer scope. This also fits with the fact that loops in rust are expressions (e.g. for loop is unit type). Finally, One-ness as a concept already exists in rust (FnOnce) so this is also consistent with other parts of the language.

This would all be consistent and coherent but unfortunately this cannot fit with the desire to use the exception based terminology. If we are to keep using a try block than it would be less strange to have "throw" or "bail" expressions to accompany them. It would be confusing as hack imo to have code like:

let b = try { 
    if cond {
        bubble "foo";
    }
}

This just looks weird. Why are we bubbling inside a try? There's no way imo this won't confuse new rust users.

Sorry to nitpick on the "bubble" suggestion, it would be the same for any other word that doesn't have the error handling connotation.

1 Like

As the canonical example, when Trying to find an element in an array, early return is the happy path.

Sure, and my point is that the most obvious way to implement it in almost any language would be consistent with what I've suggested - a loop construct with an early break.

let mut res = None;
for element in array.iter() {
    if cond {
        res = Some(element);
        break;
    }
}

The other bit here, is that it would be also awesome to allow the for loop itself to also evaluate to a type other than ()

At the risk of asking the obvious; What is wrong with just using and teaching Err(...)? as the way to do this? (Or if not "Err" whatever the appropriate type is for the particular context.)

It seems as concise as anything being described here. Sure, it looks weird the first time you see it. But it's not confusing, in the sense that you don't need to learn any new concepts to understand what is happening. If doing that becomes normalized, won't people just get used to it?

3 Likes

I for one find it very confusing, especially in the case where Err is supposed to represent success.

1 Like

If Err is supposed to represent success, you shouldn't use Result. Once the new Try trait RFC is implemented, you can use ControlFlow::Break(..)?.

4 Likes

Yes, my point was that Err is a bad fit, not that Break was a bad fit

This brought me to the example in the newly approved RFC-3058:

(using vanilla loop)

let mut sum = 0;
for x in iter {
    if x % 2 == 0 { continue }
    sum += x;
    if sum > 100 { break }
    continue
}

(using try_for_each)

let mut sum = 0;
iter.try_for_each(|x| {
    if x % 2 == 0 { return ControlFlow::Continue(()) }
    sum += x;
    if sum > 100 { return ControlFlow::Break(()) }
    ControlFlow::Continue(())
});

My ideal (using try):

let mut sum = 0;
iter.try_for_each(|x| try {
    if x % 2 == 0 { continue }
    sum += x;
    if sum > 100 { break }
    // Auto insert at the end
    // continue
});

(In general, continue in a try block returns the Output type, break in a try block returns the Residual type.)

7 Likes

The problem of using Err(x)? is that code after expr? is in general reachable, but code after yeet expr is not, so using Err(x)? or Break(x)? instead has the wrong semantics unless you special case it, which seems hard to do cleanly, e.g. what comes to my mind is to add an ad-hoc unsafe attribute on enum variants that causes Variant(x)? to be a terminator.

2 Likes