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

I was contemplating catch blocks again, and musing on the feedback that we shouldn’t use exception terminology since we don’t have exceptions, and the fact that catch blocks in RFC #243 don’t really work like catch blocks in languages like Java anyway (nor like handle in SML).

What if we renamed them fallible blocks? For example:

type SizeHint = (usize, Option<usize>);
pub fn combine(a: SizeHint, b: SizeHint) -> SizeHint {
    let low = a.0.saturating_add(b.0);
    let high = fallible { a.1?.checked_add(b.1?)? };
    (low, high)
}

The idea here is that the combine function itself is infallible: if it returns, it must produce a hint, and it cannot use ?. But inside that infallible function, there’s a fallible part where ? is allowed.

(Advance apologies to @strake if this does happen, though at least TryFrom will probably stabilize soon.)

4 Likes

I also think that it’s best not to use the catch keyword because its meaning would be different in Rust than elsewhere (languages with exceptions). A keyword that signifies that the error propagation is stopped though would be nice.

Some synonyms to catch:

accept
capture
collect
recover
resolve
take
trap 
3 Likes

I think the perspectives

  • “This is essentially exception handling, but done right, so we should use the accepted terminology for it which people are already familiar with”, and

  • “The primary connotation of ‘exception handling’ from other languages is that it is bad, so we should avoid reminding people of it at any cost”

are both valid in their own way, apparently irreconcilable, and we could probably continue bikeshedding on this basis until the End of Time.

(Compounded by the fact that the defining characteristics of what counts as “being exceptions” also varies from person to person.)

My own view is that what we have is manually propagated exceptions, and that using unfamiliar vocabulary (keywords) is a larger cost in terms of the “language strangeness budget” than re-using widely adopted vocabulary in slightly different ways, and that therefore we should use either catch or try here, but YMMV.

I think it’s notable that other languages “with exceptions” also have very different formulations of the idea amongst themselves, and that they use the usual “exception handling keywords” in wildly different ways, so I don’t think we’d be very much out of line with this; if anything, we’d just be following tradition.

(For instance, Swift uses try to mean “this code, unlike other code, may throw exceptions, and they will be propagated”; unlike Java, which uses it to mean “this code, like all other code, may throw exceptions, but here they will be intercepted”.)

14 Likes

This is how I see it too. Let me expand a bit. I think that there is a belief -- one that I have shared from time to time -- that it is not helpful to use familiar keywords unless the semantics are a perfect match, the concern being that they will setup an intuition that will lead people astray. I think that is a danger, but it works both ways: those intuitions also help people to understand, particularly in the early days. So it's a question of "how far along will you get before the differences start to matter" and "how damaging is it if you misunderstand for a while".

I'll share an anecdote. I was talking at a conference to Joe Armstrong and I asked him for his take on Elixir. He was very positive, and said that he was a bit surprised, because he would have expected that using more familiar, Ruby-like syntax would be confusing, since the semantics of Erlang can at times be quite different. But that it seemed that the opposite was true: people preferred the familiar syntax, even when the semantics had changed. (I am paraphrasing, obviously, and any deviance from Joe's beliefs are all my fault.)

I found that insight pretty deep, actually. It's something that I've had kicking around in my brain -- and I know people in this community and elsewhere have told me before -- but somehow it clicked that particular time.

Rust has a lot of concepts to learn. If we are going to succeed, it's essential that people can learn them a bit at a time, and that we not throw everything at you at once. I think we should always be on the lookout for places where we can build on intuitions from other languages; it doesn't have to be a 100% match to be useful.

15 Likes

The main concern in my mind is that Rust catch is analogous to other languages’ try (thing which might fail) rather than catch (failure handling).

8 Likes

I’ve been a fan of renaming catch to try for a while- it is much closer to other languages’ use of the terms.

This came up in the “catching functions” thread, and IIRC it was even considered in the original RFC, but was avoided due to confusion with the try! macro, which is now no longer really an issue.

12 Likes

What is the purpose of “catch” in Rust again? I’m having trouble understanding why catch is useful at all given the way ? works and Result, etc. It just seems like and exercise in, “Rust doesn’t do exceptions because everyone agrees they are dumb. Rust does Result<T,E> instead and forces user to deal with errors or implicitly propagate with ?; however, we want exceptions like every other language because they are good.”

Which all just sounds like self-contradictory nonsense. If Rust chose (wisely) not to use exceptions like C++/Java/C#, then, why is it now trying to bolt it on? What’s the goal?

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.