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

I too have thought about revisiting this. I do agree with @jsgf that try is the keyword I would have “guessed”, not catch, even though I also agree with @glaebhoerl that there is a tradition of tweaking exception-related terminology. =)

You don't always want to jump out of the whole function; I've been a proponent of catch for some time because it offers a compelling alternative to combinators like map, and_then, and friends (and keep in mind that ? operators on options and results and potentially other things).

Here is an example of some code from the compiler that uses catch:

    let _: io::Result<()> = do catch {
        let mut file =
            pretty::create_dump_file(infcx.tcx, "regioncx.dot", None, "nll", &0, source)?;
        regioncx.dump_graphviz(&mut file)
    };

I can't find it now, but recently I was working with some code that did something like:

let x = something
    .and_then(|x| something_else(x))
    .map(|y| y + 2)
    .and_then(|y| process(y))
    .map(|z| z * 3);

such code reads much nicer using catch, in my opinion (here I assuming an "ok-wrapping" variant):

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

You don’t always want to jump out of the whole function; I’ve been a proponent of catch for some time because it offers a compelling alternative to combinators like map, and_then, and friends (and keep in mind that ? operators on options and results and potentially other things).

OK, I think I understand now. Thanks for the clear example. So, it is kind of the equivalent of taking a block of code, breaking it out into it's own function that returns Result<T,E> and then putting that call in the code blocks place?

Exactly. People tend to dislike exceptions because they introduce non-local control flow, or because they are implemented with unwinding. catch does the opposite- it tightens the scope that ? applies to, using the same mechanisms as function-level ?.

3 Likes

I was recently writing some code that intermixes Result and Option. The intent of the code was "If anything goes wrong, at all, then please set this to the empty string.

I ended up with this… monstrosity:

let base_url = (|| {
            let mut toml_file = File::open(config_path)?;
            toml_file.read_to_string(&mut contents)?;
            let doc = contents.parse::<toml_edit::Document>()?;

            Ok((|| {
                let value = doc["docs"]["base-url"].as_value()?;
                let value = value.as_str()?;
                Some(value.to_string())
            })()
                .ok_or("")?)
        })()
            .unwrap_or_else(|_: Box<::std::error::Error>| String::from(""));

(The rustfmt here is… yeah also not spectacular)

This uses closures to contain the ? so I don’t return from the enclosing function. Basically, the error cases here aren’t errors for the caller, so while ? looks great, and works, I don’t want to jump out of the enclosing function. I want to catch any error and give String::new.

with catch, this would be

        let base_url: Result<String, Box<::std::error::Error>> = do catch {
            let mut toml_file = File::open(config_path)?;
            toml_file.read_to_string(&mut contents)?;
            let doc = contents.parse::<toml_edit::Document>()?;

            let value = doc["docs"]["base-url"].as_value()?;
            let value = value.as_str()?;
            Ok(value.to_string())
        };

        let base_url = base_url.unwrap_or(String::new());

well, this doesn’t quite work, since NoneError doesn’t implement Error. I remember reading an issue about this…

anyway I’m not gonna say it’s the best code ever, but that’s the basic idea.

One thing I don't like about using the "catch" keyword for this is that it doesn't really correspond to what catch does in other languages. Here, it stops auto-propagation of returning a Result and instead assigns what would've been returned as the result of the catch block, but, if no error is returned, then the result of the catch block is the result of the expression. Normally a "catch" corresponds to, "In the event of error, do this other thing instead". This is more like, "In the event of error, make the result the error, otherwise make the result the result".

For that reason, the word, "catch" just doesn't feel quite right. That being said, I'm not sure what would feel better. Some possibilities:

  • coalesce
  • fuse
  • unite
  • cohere
  • consolidate
  • unify
  • combine

In other words, a "word" that indicates that the actual result or error that might be returned will be combined/coalesced/unified into the result of the block.

Alternatively, "resultof", so it would be:

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

If we eta-reduce, I think it reads slightly better:

let x = something
          .and_then(something_else)
          .map(|y| y + 2)
          .and_then(process)
          .map(|z| z * 3);

or rustkell:

let x = do {
    x <- something;
    y <- something_else(x);
    z <- process(y + 2);
    pure(z * 3);
};

I think this is pretty optimal readability-wise.

https://philipnilsson.github.io/Badness10k/escaping-hell-with-monads/

My gripe with async { .. }, catch { .. } is the lack of generality. Of course, this was discussed (in one of @nikomatsakis's comments) in the RFC that introduced catch; and monads would have their problems in Rust (but they would also be massive enablers). I think we should be somewhat careful going forward with adding specialty features for particular monads but catch / try seems fine. I just want us not to forget about HKTs and do notation before stabilizing :wink:

Regarding try vs. catch, I thought I was in favor of the latter, but I'm coming over to the side of try because the trait is called Try after all... However "catching any Err values" also makes a lot of sense.

FWIW the original syntax I proposed in the RFC mimicked the usual try-catch form more closely: rfcs/active/0000-trait-based-exception-handling.md at 63a3c4d088374a9f3929acd77cdaa407b6d80e8f · glaebhoerl/rfcs · GitHub (N.B. whenever I used the word "exception" there, I just meant "the E component of a Result<T, E>")

That would basically be built-in syntax sugar for a .unwrap_or_else() on the result of the first block.

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

The more I think about it, the more I would like something like “resultof” instead of “catch” as far as how it reads and feels. My second choice (I think) would be “consolidate”:

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

Really like the “do” syntax from Haskell.

2 Likes

This is why I like try: normally, it corresponds to "run this block, and if it throws land here during unwinding." So in Rust, it would mean "run this block, and if it fails stop here."

This is also why I like Ok-wrapping: the block is supposed to be the "happy path," and the fact that it can fail is handled entirely by the try construct.

3 Likes

I personally prefer keywords such as do or trap. Particularly trap as I think it is sufficiently un-associated with other languages in my mind; seems like a keyword associated with errors and I think it gives an impression of a sort of containment that nicely maps to what this does - it traps any errors from being propagated higher.

I only recently heard about catch in reading Rust-related things and I immediately associated it to try catch blocks in other languages and I had to go read the RFC to understand “oh, this makes sense, it’s not like other languages”. I don’t think that familiarity with the word is something beneficial in this case.

4 Likes

Agree!

Also, "trap" sounds pretty good:

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

But, I still like "resultof" because it is getting the "Result<T,E>" of the block assigned to the let variable. Once someone understands that the error propagation mechanism of Rust is Result<T,E> "resultof" should sound intuitive (or at least that's my story and I'm stickin' to it) :slight_smile:

That being said, I like "trap" more than "try" or "catch" for exactly the reasons you gave.

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.