Syntax for returning early with an error

In this design meeting there was a discussion about the syntax to use for returning early with an error. The considered keywords were fail, throw, raise, yeet. I couldn't find any more recent discussion.

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

Many crates have a macro called bail! for this, so a bail keyword is out of the question, since it would cause a lot of churn for these crates.

throw and raise are associated with exceptions, which Rust doesn't have, so these names are suboptimal. fail has the disadvantage that it implies an error, but the keyword should work for any type that implements Try, so the type that is returning early isn't necessarily an error. I'm not sure what yeet is supposed to mean, I guess it's a made up word.

I think that it's time to decide now, because if we want a keyword for Err(...)?, that keyword requires a new edition, and the next edition is approaching fast.

After a bit of brainstorming, I came up with a few more candidates:

  • excuse
  • quash
  • oust
  • dismiss
  • depart
  • cancel

I particularly like excuse, but I'd like to hear more opinions and suggestions.


Summary of the discussion so far

  • The design I had in mind doesn't work with the new Try trait proposal as pointed out by @steffahn. So if we want this feature to be generic over the new Try trait, that could be done with a new-type, here called Yeeted.

  • A yeet foo statement could be desugared like so:

    // with the current Try trait:
    return Try::from_error(From::from(foo));
    
    // with the proposed new Try trait:
    return FromResidual::from_residual(Yeeted(foo));
    

    The desugaring would be special-cased in try blocks, so we only exit the try block and not the whole function.

  • Yeet is a slang phrase meaning to throw with force.

  • yeet; can be syntactic sugar for yeet ();.

  • Instead of a keyword, this could be implemented as a function:

    yeet(foo)?;
    
  • A keyword can't be reserved before the RFC for that keyword is accepted, so the yeet keyword (or another one) most likely won't make it into the 2021 edition

  • The motivation for this feature is not entirely convincing

  • Apparently there's a Grand Evil Plan to make break work everywhere and make return syntactic sugar for break 'fn. While the merit of that is unclear, it would be useful to be able to break out of a try block.

  • bail! macros in common error handling crates don't work as one would expect within try macros (they return from the function). But it would be possible to rewrite them to use ? instead.

  • Err(...)? currently triggers a clippy style warning, except in try blocks. However, that warning could be disabled if desired.

  • Exiting a scope and value wrapping are two orthogonal concerns, so they arguably shouldn't be combined with a single keyword.

You may edit this summary.

4 Likes

An example, using the anyhow crate:

fn foo() -> anyhow::Result<String> {
    let result: anyhow::Result<String> = try {
        if !something_is_valid()? {
            excuse anyhow!("this is an error");
        }
        "value".into()
    };
    result.context("additional information")
}

I’m not sure what the keyword is supposed to do. Can you give or link an explanation? Is it supposed to be Result-specific?

There’s also the alternative of using a function

fn excuse<E>(e: E) -> Result<!, E> {
    Err(e)
}

then the expression to use would be “excuse(...)?”.


With the new Try trait you will (AFAICT) also be able to define a struct Excuse<E>(E) such that Excuse(...)? becomes yet another equivalent alternative.

1 Like

No, sorry for not being more specific. It can early return any type that implements Try. The desugaring is

excuse foo;
// becomes
return Try::from_error(From::from(foo))

except if it's in a try block, it only exits the try block, not the whole function.

Well, this won’t work with the new Try trait v2 anymore though, AFAICT.

Or rather, the “equivalent” would be to only support Result.

If try_trait_v2 is accepted, the desugaring becomes

return FromResidual::from_residual(foo)

why do you think this won't work?

because then foo would need to be of type Result<!, E> for e.g. a Result context. (Or foo: Option<!> for Options, etc..) You’d need to write excuse Err(...). I.e. the desugaring not “equivalent” to the suggested return Try::from_error(From::from(foo)) desugaring with the try-v1 trait.

I don’t think the ergonomics of try-v2 are built in a way that it’s usually not intended to manually provide values for the Residual type.

Hmm, the RFC explicitly says

This is forward-looking to be compatible with other features, like try {} blocks or yeet e expressions or Iterator::try_find , but the statuses of those features are not themselves impacted by this RFC.

One should question what exactly the “compatibility with yeet e” is supposed to mean.

Edit: Oh, wait there’s a paragraph about it hidden at the end of the thing!

So with the option

  • It could put the argument into a special residual type, so yeet e would desugar to something like FromResidual::from_residual(Yeeted(e)) .

one can archieve yeet-compatibility for any type Foo<Bar, T> by implementing

impl<T> FromResidual<Yeeted<T>> for Foo<Bar, T> {...}
3 Likes

I like this. One could add another type, e.g. YeetedNothing for supporting an argument-less yeet; call. Something like Option would implement this. The implementations for Result and Option would be:

impl<T, E> FromResidual<Yeeted<E>> for Result<T, E> {...}
impl<T> FromResidual<YeetedNothing> for Option<T> {...}
1 Like

Early return for success never became a streamlined thing. Assuming Try trait v2 goes through, people who want it will presumably make their own types so that ? returns the success case. One can also imagine implementations where the early return is neither an error or a success, really. (Arguably this is already the case for Option.)

So the colour of the yeet shed should probably be error/success/other agnostic.

This went poorly last time. I don't know if there was ever a resolution on whether pre-reserving keywords should or shouldn't be done, though. The team seemed split on the matter.

1 Like

This isn't inherently true. We could introduce such a keyword in an edition. This was an issue for try, for instance. And at one point I believe some error-handling crates used a throw macro as well.

I don't necessarily think it's the ideal choice, but it does have the advantage of not being specific to error-handling terminology.

Swift uses throw for its fallible function feature (which doesn't rely on exceptions) so there's at least some prior art on that. I think throw heavily implies an error though which doesn't seem desirable.

Of the listed key words bail expresses the intent the most clearly imo. I would personally rather have to update code to move away from an existing bail macro when moving to the new edition than have an ambiguous keyword, though I realize that may not turn out to be viable.

Another option might be combining return with the ? operator for return? foo since it would be replacing/enhancing return Err(foo)?. The difference between that and return foo? would probably not be obvious at first glance. It also doesn't really match the semantic meaning of ?. The only real benefit it has is dodging the issue of picking a new keyword.

2 Likes

You're right that bail would be possible, but I think it's a bad idea. The try macro could be replaced with ? automatically because it was part of the standard library, if I'm not mistaken. The bail! macros however can't be replaced by a bail keyword when running cargo fix. And ? was introduced long before try was reserved, so users had enough time to migrate.

Moreover, the new keyword has different semantics than current bail! macros (well, at least the one from anyhow) within try blocks. This could cause confusion.

I honestly wouldn't mind if yeet were the actual final keyword, and support the hypothetical #![feature(shitposts)] that would make it work even if it isn't

For those unaware, "yeet" can be roughly defined as "the opposite of yoink" and more formally defined as

an exclamation of excitement, approval, surprise, or all-around energy, often as issued when throwing something.

(I actually don't quite disagree with dictionary.com here and more only hear/use it as the verb opposite of yoink, but I'm only in one place with one friend group. They provide some more history of how the term emerged.)

2 Likes

Not commenting on the choice of name here, so assume for this post that we’re using yeet.

Assume we eventually want to use yeet e for

return FromResidual::from_residual(Yeeted(e))

(well, except in a try {} block where return is inaccurate).

An intermediate step could still be a function

fn yeet<E>(e: E) -> Yeet<E> { Yeet(e) }

returning an unstable/unnamable type Yeet together with an implementation

impl<E> Try for Yeet<E> {
    type Output = !;
    type Residual = Yeeted<E>;
    fn from_output(c: !) -> Self {
        match c {}
    }
    fn branch(self) -> ControlFlow<Yeeted<E>, !> {
        let Yeet(e) = self;
        ControlFlow::Break(Yeeted(e))
    }
}
impl<E> FromResidual for Yeet<E> {
    fn from_residual(Yeeted(e): Yeeted<E>) -> Self {
        Yeet(e)
    }
}

this could be introduced to the prelude, this way we can use yeet(e)? in place of yeet e until a keyword is eventually introduced. I think this is not too much of a loss after all, and the keyword can comfortably wait for the next edition (2024) without any problem. Once yeet keyword is introduced the r#yeet function can be deprecated.


To go into details why this approach works: the ? desugaring for yeet(e)? would be

match Try::branch(yeet(e)) {
    ControlFlow::Continue(v) => v,
    ControlFlow::Break(r) => return FromResidual::from_residual(r),
}

which, by inlining Yeet<E>::branch, reduces to

match ControlFlow::Break(Yeeted(e)) {
    ControlFlow::Continue(v) => v,
    ControlFlow::Break(r) => return FromResidual::from_residual(r),
}

which can be further simplified, back the original/intended yeet e desugaring

return FromResidual::from_residual(Yeeted(e));

so that the use yeet(e)? of a yeet-function is indeed fully equivalent to using the (hypothetical) yeet e keyword

(in all of the above, I’m still ignoring try {} blocks by using return; the argument still holds in the context of try block if you replace return with some hypothetical return_from_try in the desugarings for ? and yeet)


Edit: For some further considerations,

  • as already proposed in later comments in this thread, this means that there’s also the option of never actually introducing a new keword, instead just introducing a yeet function that is supposed to be used in yeet(e)?-style expressions;
  • the types Yeet and Yeeted above can be unified if we want, i.e. Yeeted could be its own residual type, although I’d probably prefer the name Yeet for the type in this case;
  • one can get rid of the yeet function, especially since a function yeet(e) that evaluates to Yeet(e) seems very redundant.
    • the two options there would be
      • either give the struct a lowercase name struct yeet<E>(E);
      • or use Yeet(e)?-style expressions (with a capitalized Yeet).
    • In my opinion, all three options (yeet function / lowercase tuple struct / use capitalized “Yeet(e)?”) have advantages and disadvantages.

I’ll reiterate my initial statement here: you have to replace the words yeet/yeeted with whatever verb we end up picking in all of the above.

3 Likes

What about oops?

fn foo() -> Result<(), String> {
   oops "something went wrong"
}
2 Likes
Aside

Minor nit: in the same fashion that return; is sugar for return (); and break; is "sugar" for break (); (when the latter is allowed: alas break (); is not allowed for for loops :disappointed:), the most intuitive / consistent approach for yeet; would thus be to stand for yeet ();, so that we'd have type YeetedNothing = Yeeted<()>;, at which point a struct Yeeted<Payload = ()> with a default type parameter would allow us to simply name it the Yeeted type.

1 Like

I know about the return; standing for return (); thing... I didn’t think like it’s the most ideomatic for yeet, since it would on one hand introduce special short syntax that can interact with Result<T, ()> (which is a type that e.g. clippy doesn’t seem to like very much), and on the other hand it allows for yeet (); in an Option context which seems weird IMO since nothing about Option says “I’m related to the () type”.

1 Like

Note that this was considered back in 2018, and it was decided then not to do keyword reservations going forward for features that have not yet been accepted: https://github.com/rust-lang/rfcs/pull/2441#issuecomment-395256368

The current conversation is about something like reserving k#keyword so that if yeet gets picked, it'll be usable as k#yeet until the next edition can reserve it fully.

1 Like

yeet (); or just yeet; would be useful in a function or try block that returns an Option<?> or ControlFlow<(), ?>.