Bikeshed: Rename `catch` blocks to `fallible` blocks

I’m not particularly familiar with the exact details of the catch block - does it always return a Result<T, E> or is that something that may change through a trait similar to Try? If it wasn’t always going to return a Result<T, E> then resultof might not be ideal. Also there aren’t any other keywords AFAIK (that are in use, there are some reserved ones) that are two words concatentated.

Short version: I'm also leaning towards try as the keyword for ? scoping. The behavior of these ? blocks is much closer to how existing languages use try than it is to how they use catch, and I feel like the word "try" is much less tied to any specific notion of exceptions than "throw" and "catch" are.

Now for the much longer version, where I shall begin by bikeshedding what you think the bikeshed even is:

This is actually very far off what I thought the "standard" perspective/terminology was for all of these things, outside the realm of research languages and academia and programming language enthusiasts, so maybe it'll help if I spell out the terminology and narrative that I'm used to from my C++/Javascript-based corner of the not-yet-Rust-using industry.

In my experience "error handling" is the generic term for all the stuff we're talking about, and the hardest part of it is propagating errors when the error simply cannot be handled right when it occurs. The two major approaches are "return codes" that are explicitly, manually propagated, and "exceptions" which are implicitly, automatically propagated. Hence the metaphor of throwing and catching; once thrown an exception does not stop flying up the stack until it hits something that's ready and waiting to catch it. Return codes stay right where they are unless code is written at each layer to propagate them. And this leads into the usual narrative of why exceptions are a big improvement over return codes (at least the way they turned out in C): exceptions are a lot harder to ignore on accident, and failing fast is better than delaying the inevitable.

With Rust, everyone's still forming their own narrative, but mine is that Rust made pervasive, explicit return code propagation feasible by reducing it to literally one single character. Manual error handling was always theoretically better, since there's no longer this invisible extra codepath that behaves very differently to all other code in the language. And now that the ergonomics issue is solved, it's also better in practice. In particular, that theoretical benefit of more uniform semantics helps when you want to do any non-trivial error propagation, such as bundling up all the errors that happened in various worker threads in a way that the main thread can figure out how to report them properly. Automatically propagated errors make that very hard to do correctly. When typical, trivial propagation is ergonomic enough to do manually all the time, that makes it straightforward to do non-trivial things when you need to, because the same language rules apply to error values as any other kind of value. This is a lot like how in Javascript the async/await keywords make the typical, trivial control flow for async code very ergonomic, but you can always fall back to manipulating promises if you need some non-trivial async control flow like a Promise.all or Promise.race or whatever.

I agree with all of this in principle. Some keywords and jargon have a lot of intrinsic "wiggle room", vagueness, or variance among existing languages, such that it's a net benefit to reuse them for a similar-but-not-identical feature in our shiny new language. Such as "try". But that doesn't mean there aren't keywords and jargon out there with a specific enough established meaning that it would do more harm than good to apply it to something fundamentally different.

I dislike the use of catch specifically because I believe that most mainstream programming languages use "throw", "catch" and "exception" to refer unambiguously to an error handling mechanism where propagation is implicit, as opposed to the manual propagation of return codes. This is certainly not true of all programming languages ever made, and there's plenty of variation in how exceptions have been implemented even among these mainstream languages, but I do think the typical working industry programmer discovering Rust is more likely to associate "throw", "catch" and "exception" with implicit propagation mechanisms in particular than with error handling in general. So I still think that using those terms for Rust would be a net loss (unless we apply them to panics, of course, which catch_panic already does).

But I do not feel that way about "try". I'm not entirely sure how to justify this (though it's probably impossible to convincingly justify any claim of this sort anyway), but I think "try" has a lot more wiggle room than "catch" does, and it would be perfectly fine to apply "try" to Rust's ?-scoping blocks. I suspect this is because in mainstream languages, the "try" construct "doesn't do anything"; it's merely setup for a later keyword that does the real work. In a try/catch construct, it's the catch that actually does something. In a try/finally, it's the finally that actually does something. With Javascript promises, there is no syntactic need for a "try" keyword, so it disappears completely; the interesting work is all done by methods like .catch() and .finally(), and there simply is no .try().

Maybe I'm wildly biased and none of this rambling applies to "the programming community" as a whole, but that's where I was coming from when I said I didn't like the use of catch for this feature.

11 Likes

So, given that you tend to agree that it would be bad to use a keyword like "catch" to mean something fairly different from most mainstream usages, and that "try" also is not quite the same usage in Rust as it is in mainstream languages, what are your thoughts on (from above posts by myself and @davidtwco) for:

  • resultof
  • trap

My thoughts would be that what Rust is doing is different enough from what most mainstream languages are doing with "try/catch/throw/raise/finally" that avoiding any of those words would be a good idea and choosing a keyword that captures (Hey, there's another possibility "capture"!) the essence of what Rust is doing and should have some intuition with it that would align with someone who is learning Rust. So, possibilities might be:

let x = resultof {
   let y = something_else(something?)?;
   process(y + 2)? * 3
};
let x = trap {
   let y = something_else(something?)?;
   process(y + 2)? * 3
};
let x = capture {
   let y = something_else(something?)?;
   process(y + 2)? * 3
};
let x = try {
   let y = something_else(something?)?;
   process(y + 2)? * 3
};

Of all of those, which seems to make the most sense and intuitively match what is going on with Rust error handling?

My argument against "try" would be that because of its usage in mainstream languages it suggests that the body might "throw/raise" an exception, but, that is not Rust is doing. Better to avoid the confusion (IMHO).

EDIT: Another thought, since the "catch" concept for Rust will be the equivalent of breaking out the block contents into a different function/method that returns a Result<T,E> and replacing the block with a call to that function/method, then perhaps, "returned" is the keyword we want:

let x = returned {
   let y = something_else(something?)?;
   process(y + 2)? * 3
};

Though, I really think that "resultof" is more clear and intuitive.

Another advantage, it would seem, of using the "returned" keyword would be that this could also subsume the idea of "returning early out of a block" (which is kind of what is going on here). For example:

let x = returned {
   if prereqnotmet() { return 0 }
   let y = something_else(something?)?;
   process(y + 2)? * 3
};

This sort of "return a value from a block" could be used in conjunction with the "returned" keyword to "early return" a value for the block. This unifies (I think) two concepts that don't initially seem like the same thing, but, in the case of Rust, are the same. It also makes clear that Rust is NOT doing "Exception Handling", but, is instead doing, "Return Value or Error".

1 Like

+1 for try

@gbutler, ? will be able return anything that implements the trait Try, not just Result<T,E>.

1 Like

OK, but, it will still be returning some kind of value, not throwing/raising an exception, which is the traditional association of “try”. That’s why I proposed, “returned” because it aligns with what is going on. A value (either a Result<T,E> or something else that can be the desired value type or something returned as an error by ?) is being returned from the block as the value of the block. No exception is thrown or raised which is what most people will think if they’ve come from other languages.

I like trap the most, because it is completely unrelated to exception terminology from other languages and hence not confusing. It clearly communicates what is happening and sounds like an error-handling-related thing.

It is also short and concise, unlike returned, etc. I like concise and clear syntax without excessive typing.

Otherwise, yes, try is definitely better than catch.

3 Likes

Someone coming from another language (JS, C#, C++, Java, D, etc, etc) will all be wondering, “I see ‘try’, but, where is the ‘catch/finally/???’” and how come I can’t “raise/throw” inside the “try” block.

EDIT: Again, I’ll “Raise” the objection that using any of “try/catch/finally/throw/throws/raise/raises” in relation to this is going to cause endless confusion and will result in constant acrimonious criticism of Rust due to its co-opting of keywords to have a meaning not in line with the mainstream. It will also become a discussion point along the lines of, “Why is Rusts EXCEPTION HANDLING so F----ed up? Why are Exceptions not thrown? Why is it sometimes a return value and other times you can “try” with an implied “catch”? Don’t these Rustaceans have a clue as how EXCEPTION HANDLING works? Why doesn’t it automatically unwind the stack? Why???..etc”. I can hear the wailing and gnashing of teeth already.

I don’t think it’s anywhere near that bad. The fact that there isn’t a corresponding exception handler or finally block attached to this thing is a pretty obvious tell that something is slightly different, and that it’s time to look up what that is.

We’ve also already got the Try trait, which you might easily think of as the operator-overloading trait for try+?, analogous to Add for + or Index for []. In fact, the ? operator used to be a standard library macro called try!, so the name isn’t even new.

And for that matter, Rust already has a reputation for doing error handling “differently.” Nobody is going to complain about this any more than they already do just because we introduce try blocks instead of resultof blocks.

6 Likes

I'm mostly lurking here and wasn't really active in previous catch-related changes/RFCs. Truth be told I'm not really satisfied with what's currently proposed and since this will inevitably land at some point and I really like @scottmcm 's suggestion, I'd like to defend this bikeshed will all the spare bike tires I have :grin:

First of all, I think it perfectly describes what fallible { ... } blocks are and what should do. I personally parse it as a 'fallible' expression, which naturally means 'expect a value representing a possible failure'. This aligns with the semantics, where the expression inside will always return either a Result or an Option (or other Try implementers?).

When talking about evaluating an expression returning a concrete fallible value this also somewhat naturally implies short-circuiting (how would you evaluate the expression further if you have an error early and what else would you return?), which is what it's for in the first place.

There's also another teaching/ecosystem/community benefit - people are already used to the term and despite not being as simple as try, associate it with something that returns a Result or what can fail in general. A good example might be the fallible collections RFC, fallible-iterator crate or the fallible term/Infallible used in TryFrom.

Regarding reusing the widespread exception handling syntax and:

I think exceptions are a pretty hot topic when talking about programming languages and very polarising. In this particular case I think reusing the catch syntax might do more damage than good, as I believe one could expect that the underlying code can throw exceptions and introduce non-trivial control flow where in fact we just limit the scope for short-circuiting evaluation.

However, if we were to use the exception-related syntax, I'd also lean more towards try (we 'try' to do something, rather than use it to handle errors; basically what @Ixrec said and we have a Try trait for short-circuiting).

4 Likes

EDIT: re-reading what I wrote here, I think it may have come off more accusatory/harsh than I intended. I only intend to point out my strong feeling that “try”/“catch” or related words just don’t feel right (to me) and I’m wondering how much of the support for “try” (or “catch”) comes from just feeling it should be what is used because it is generally what is used as opposed to having a strong feeling one way or the other.

I’m still not sold on the idea of using “try”. It seems to me that:

let x = whatever_keyword_is_chosen {
   let y = something_else(something?)?;
   process(y + 2)? * 3
};

Is really a short-hand for:

let x = ApplyLambda( || {
   let y = something_else(something?)?;
   process(y + 2)? * 3
});

Which, again, argues for using something like “returned” or “resultof” to emphasize the notion that Rust handles error conditions by returning Error/Fallible values or the desired Value from functions, not by raising errors, stack unwinding, etc.

NOTE: In the above I’m using some pseudo-code just to convey the notion of what I’m getting at.

I honestly feel like everyone arguing for “try”, “catch”, etc. wish (perhaps without even being completely aware of why) that Rust just had exceptions like other languages and really don’t like the fact that Rust uses Result<T,E> etc. It seems like everyone just wants to make it look like exceptions in other languages even though it isn’t. Maybe it is even subconscious (i.e. “it feels like it should be that way because that is what [we’re] used to”).

Personally, I really like the direction of Rust error handling and think it makes sense to emphasize the fact that it is returning value/error not raising exceptions/stack unwinding.

In fact, because “Panic!” is effectively an “Exception” that can’t be caught (akin to a RunTime exception in Java that shouldn’t be caught with the additional restriction that it CAN’T be caught, except at a thread boundary) that making it absolutely 100% clear that Rust error handling ARE NOT exceptions is the most advisable thing to do.

Again, this is just my opinion, but, and opinion I strongly hold. I would ask everyone to really consider why they want something like “try”? What makes you lean in that direction? Is it justified by the nature of what Rust is doing/embracing or is the only justification some notion of using terminology familiar to other exception-based languages? If the latter, why is that a good thing? Really think about it.

I love Rust error handling. I hate exceptions as the normal mode of error handling; panic is fine. I fully understand your objections and I’m familiar with error handling in several other languages, but try does not make me assume exceptions. I still prefer try and others have already expressed my reasons for why. I would also accept fallible without complaint.

To me, try means: intercept any control flow actions due to errors in this block and continue on the next statement/expression. It means the same thing in Rust as other languages I’m familiar with.

1 Like

Just to be clear, I really like Rust’s error handling story, that it uses Result values and similar, so I think we both agree here. I also would really like if we move on from catch/exception terminology here.

I already explained why I feel fallible sort of naturally conveys the semantics that will be used.

However, if we were to use try/catch, I’d prefer try much more, simply because it’s more innocuous and natural ('try that, at worst we’ll get Err') rather than catch ('catch what? that’s like catching exceptions, but where’s the regular catch block where I handle incoming error?).

To sum it up, go team fallible :tada:, but we can also try try! (OTOH fallible is considerably longer…)

4 Likes

I agree that “fallible” as the keyword works much better than “try” or “catch”.

I was going to object to try on the basis that it seems like too prime a variable name to be claimed by a keyword, but I think in practice I’ve never used it and the try!() macro has already claimed it (in a figurative rather than literal sense) for quite similar purpose. But maybe that’s just Rust old-timer thinking.

(If I could have my wishes magically enacted retroactively, Rust would use typedef rather than type - it’s my number one keyword/desired symbol collision.)

1 Like

I like try.

  • It’s a syntax sugar, so the shorter keyword the better (do might work too, but it’s not obviously error-handling-related).
  • In languages with try/catch the “run this code until an error” part is contained in the try block, not the catch block.
  • Swift has similar try? for wrapping exceptions in optionals. It also has do { try } catch error { }, so compared to it Rust’s try would be more similar, and catch more confusing.
10 Likes

Both these points seem misleading.

  • The fact that the try block means “continue until error” is misleading because it doesn’t mean “and contain the thrown error”. In languages with try-catch, try only serves to put a bound on where fallible code may occur—on its own it doesn’t contain the thrown errors. The former is the role served on a more granular level by ? in Rust (and try in Swift).
  • In Swift, try means the same thing as ? in Rust: a fallible operation that evaluates to the success branch, with the error branch being handled non-locally. Focusing on try? coalescing the success and error branches into a single value ignores the fact that this meaning is provided by the ? and not the try.

In languages with try-catch, catch applies to the preceding block. The block following catch consists of handlers describing which errors it can handle and how. In Rust’s expression-oriented model, it makes sense for catch to be a prefix operation rather than a postfix one, especially since Rust doesn’t need a place to put inline handlers: it doesn’t need to evaluate to (), it only needs to evaluate to Result<T, E>.

I think the meaning of try in context is quite easy to grasp:

let result = try {
    // Here's an attempt at doing something.
    // Needless to say, it may succeed or fail.
}

The meaning should be obvious to anyone coming from a try...catch background. Catch isn’t needded because the value/error is captured as the value of the try block.

14 Likes

As someone just picking Rust up (6 months?), who flits from language to language, I actually appreciate the familiar terms such as try, do and catch, even though the proposed semantics are different in Rust than in other langauages. The fact that the terms point in the general vicinity of error handling provides a toe hold which is useful when starting out.

I personally find both fallible, resultof very noisy. I am much happier typing enum than taggedunion, even though enum behaves differently in Rust than in say c or c++.

I prefer monosyllabic keywords (so I am happy with trap) as they seem to be easier to soak in; they are more “restful” on the eyes when scanning code (at least for me. subjective i know). We are going to be typing and reading these a lot, so making them short and tidy is paramount IMHO.

Thought I would provide a “newbie” perspective. Thanks for all the great work on Rust.

7 Likes

One point which supports this argument is the presence of try_something functions in the standard library. These functions all return Result. By this pattern, a try operation is a fallible operation that returns a Result value representing the success and failure cases. This understanding works for both try_ functions and
try { ... } expressions.

One question I have is how this would translate into a function-level syntax (which has been proposed from time to time). With catch you can have -> Result<T, E> catches or -> T catches E or variations with just catch in them instead. try doesn't really suggest anything, except maybe try fn ..., which would stutter in the case of try fn try_something() .... I know some people are partial to throws, but that doesn't accurately describe something a function can do (only the ? operator can actually "throw").

8 Likes

There are many opinions here. How about:

fn foo() -> Result<T, E> try {
    ...
}

That makes it similar to:

let result = try {
    ...
}

The idea is that try belongs more to the block than the fn header, if that makes sense.

With fn foo() -> T catches E the function implementation affects how the function signature is declared. Therefore I would prefer if the return type declaration remains Result<T, E>, even though there are some good arguments for T catches E

At least I don't see any problems with try { for function blocks.

1 Like