RFC : Try-Catch Blocks

@proc yo bro, high five :smile:

@droundy The Drop trait approach is valid only if the cleanup operation is meant to be executed either the operation failed or succeeded. It would act as a finally clause rather than catch. Also It would require a type designed for the operation.

As many mentioned previously, the only option today is to work on some macros or compiler plugin.

My solution was… to switch to D. I ended up with the conclusion that Rust is not ready yet for large scale, high level projects. So far it is a good alternative to C, yet not C++. Looking forward to next Rust versions.

@raizam Of course, if you can afford a GC, D is a fine language to choose.

I ended up with the conclusion that Rust is not ready yet for large scale, high level projects.

Such as rustc? servo? Piston? Iron? There are many other projects of various sizes and levels. Given this, your conclusion seems a bit premature.

2 Likes

Not if you provide a method on the guard to “disarm” it.

No, you just need a type that’ll execute a callback on Drop; like this Defer type.

Also, having spent about half a decade using D, I’d be more inclined to use Rust on a large-scale project than D.

4 Likes

@llogiq, @DanielKeep Thanks for answering my salty comments with good arguments.

I’m not good enough to understand this in depth, but it is inspiring.

I do see what you mean somehow.

Here is what I’ve just found:

https://doc.rust-lang.org/std/thread/fn.catch_panic.html

Is this… what I was looking for?

1 Like

Absolutely not. That’s only for specific use cases, like isolating unwinding when doing ffi.

Error handling in Rust is done with try! and Result and is quite convenient. I wrote a lot about it here: http://blog.burntsushi.net/rust-error-handling/ I recommend giving it a shot with a careful read.

3 Likes

Hey, big +1 for the Defer type! I’d like to see it in a separate crate, so we can use it without code duplication!
That’s one of the missing parts (reminds me of defer keyword in Go, but better, as it’s typechecked and implemented with the language, which is another sign of how Rust is much more powerful than Go).

crate scopeguard exists, it doesn’t do the Option dance like Defer does, but it’s the same idea.

Scopeguard implements deref and passes through, because I use it to run a finalizer on an object that I otherwise modify. So the guard owns it and I borrow it.

Edited: The spotlight forced my hand, it now comes with documentation :smile:

1 Like

Good to know about it, thanks!
The problem with discoverability of such small useful crates is poor docs. It’s not just scopeguard’s problem, there are a lot of crates with just a couple of lines of description, no documentation link, and just one phrase in readme file at best, without even helpful usage examples. So even if you manage to find it somehow, you don’t know how to use it, and opt for another, maybe worse crate, but with better docs.

5 Likes

Yes I understand. I just dumped it out since I happened to show it on stackoverflow and got a request to package it :smile:

Perhaps TWIR should have a “crate of the week” section? cc @nasa42

We could have a nomination/voting process similar to QOTW. Otherwise we could do a sticky thread like the “what’re you doing this week?”

Don’t know but after reading a little I was thinking of (just on the top of my head)

let file: File = sanitize(read, defaultFile).stopon(socketError, emptyNotWriteableFile).sanitize(NotFound, defaultFile);

or

Thought it could be possible to have File or SocketError if wanted:

let file: File|SocketError = read(some).stopOn(SocketError).sanitize(notFound, with: defaultFile);

So it would make clear that you want to do:

  • sanitize an error (for example provide a anon login user).
  • unrecoverable error with something like stopOn… so if you are writing a lib, notify the user of that lib that the responsibility is on their side.

I use sanitize, because it is on place of error (also I think you could sanitize different errors on same call), and the “sanitizer” would be a value or a method that produces a value (never an error or option).

Thought I think this is more like open_or, but naming it this way, it could be easily searched on the project where you have sanitizers and break of flows, also I think options and results are used to more than signal/handle errors/exceptions??? (so it will be easier to search a project for this type of things and don’t wander in other type of usages).

Also I think if there are code snippets that show why now doesn’t look “nice now” a piece of code, would be nice to see it pasted here.

Personally I would want some sugar for and_then so code could be written in a more succinct way:

let mut data = Vec::new();

let num_bytes = do {
    let file = File::open("foo.txt");
    file.read_to_end(vec)
}.expect("Failed to open and read foo.txt");

But the best thing would be if the notation is not tied to a particular type. Then custom types can be used when needed which can provide different semantics (and not necessarily just for error handling, can be any kind of state-management really). This allows for using Result, Option and even Iterator and any other type which satisfies some basic conditions with this notation.

This will enable error handling very similar to try-catch where the error itself does not need to be handled inside of the code block. Instead by treating this notation as an expression it can be used in match, if let, try! and other constructions to actually manage the error itself after the computation.

Essentially I want Haskell’s do-notation or Scala’s for-notation for managing computations in a context, this should solve the problem with try-catch as well as other somewhat unsightly uses of chaining notation.

We need higher kinded types before we can get ‘real’ do notation, but there’s also https://crates.io/crates/mdo

I have been working a bit on my own do-notation for my parsers, as you might know, so I am definitely looking forward to getting HKTs at some point so that notation like this could be made general over anything which is a monad.

Just trying to show that there are more ways to handle try-catch than the classical Java/C++ style try-catch.

1 Like

I would suggest the following concept for try catch semantic ...

Consider the following code:

union SomeFunctionMultipleError {
    err0: Error1,
    err1: Error2,
}

struct SomeFunctionFnError {
    index: u32,
    errors: SomeFunctionMultipleError,
}

fn some_function() -> Result<i32, SomeFunctionFnError> {
    if 0 == 0 {
        Ok(2)
    } else {
        Err(SomeFunctionFnError{ index: 0, errors: SomeFunctionMultipleError {err1: Error2 {id0: 0, id1: 0, id3: 0}}})
    }
}

union OtherFunctionMultipleError {
    err0: Error1,
    err1: Error2,
    err2: Error3,
}

struct OtherFunctionFnError {
    id: u32,
    errors: OtherFunctionMultipleError,
}

fn other_function() -> Result<i32, OtherFunctionFnError> {
    if 0 == 0 {
        Ok(2)
    } else {
        Err(OtherFunctionFnError {id: 0, errors: OtherFunctionMultipleError {err0: Error1 {id0: 0, id1: 0}}})
    }
}

This is the code that could be generated by Zero-Overhead exceptions in Rust with following syntax feature:

fn some_function() -> i32 throws Error1, Error2 {
    if 0 == 0 {
        2
    } else {
        Error2 {id0: 0, id1: 0, id3: 0}.throw
    }
}

fn other_function() -> i32 throws Error1, Error2, Error3 {
    if 0 == 0 {
        2
    } else {
        Error1{id0: 0, id1: 0}.throw
    }
}

or even these errors could be deduced by compiler implicitly:

fn some_function(i: i32) -> i32 throws { // Implicitly throws Error1, Error2
    if i == 0 {
        2
    } else if i == 1 {
        Error1 {id0: 0, id1: 0, id3: 0}.throw
    } else {
        Error2 {id0: 0, id1: 0, id3: 0}.throw
    }
}

fn other_function(i: i32) -> i32 throws { // Implicitly throws Error1
    if i == 0 {
        2
    } else {
        Error1{id0: 0, id1: 0}.throw
    }
}

Please don't revive old threads.

The current state of try blocks can be found here: https://github.com/rust-lang/rust/issues/31436

It's quite possible that most people on this thread know something you don't:
That exceptions are a liability in a programming language rather than an asset.
They are similar to OO inheritance in that way.

Here are some problems with exceptions:

  1. Automatic stack unwinding when an exception is thrown, including when it is not suitable. I'm aware that you're supposed to use catch for this, yet this issue still prevents large amounts of code from actually reacting to the exception and fixing the state. Instead all they can do is propagate the exception further. This is avoidance behavior and it makes for brittle code.
  2. Try-catch syntax is awful because it forces indentation upon you and you'll be continuously fighting the language (e.g. Java, C#) and its tooling if you want e.g. no additional indentation. kind of like in a switch/case statement. In languages like Java and even Rust where the meat of the code is usually at least already fairly indented (e.g. the body of a method begins at 0-indexed column 8), programmers are better off without even more indenting syntax. This is because one important effect of additional indentation is that it gives you an ever-increasing amount of context you need to keep in your head at once which is undesirable because it is entirely avoidable.
  3. Try-catch wants to conflate propagation (try) and handling (catch). It doesn't quite succeed (try and catch are sort-of separate) but the inability to just catch without trying, or (in most cases) try without catching is a net negative.
  4. Try-catch notation is boilerplate-heavy and thus inconvenient.
  5. Depending on the implementation, exceptions are exceedingly slow. So when the code processing of some data hits an error path somewhere, it can be slowed down tremendously. I've people justify this with some faulty reasoning like "yeah but it's only in exceptional cases that Exceptions are/should be used". And while there might be truth to that claim when it's interpreted as normative, in practice once exceptions infect (yes, infect) a language they tend to be used all over the place.
  6. Exceptions conflate control flow and error management. Whose idea that was, I'll never know, but it wasn't a bright one. Reason 1 already alluded to this, but doesn't make it explicit.
  7. Exceptions bring with them the whole "checked vs unchecked" exceptions debate, which is about as useful as debating whether a snail or an F1 race car will win a race.

Some of these points have been made by others already.
So both on the syntactic and the semantic level, exceptions like the ones in Java and C# are entirely undesirable.

"So what's better than exceptions?" you may ask. Well, Rust already has it: non-automatically propagated error values, that also don't throw control flow into the mix. The reason is that this allows the programmer full control of which errors are handled, which are propagated and which ones lead to a panic, where an error is handled, and how it is handled.

To be honest, as a consequence of the above reasons, the entire terminology surrounding exceptions has turned sour for me: throw, exception, catch, all of these are just reminders of inferior technology that should be put to pasture once and for all in ecosystems that can get away with it. That is also why I'll always oppose attempts to introduce that kind of terminology as keywords into the language. This holds doubly when it's supposed to work with Result-based error handling, because invariably it's pitched as "but programmers are already comfortable with that terminology" while in reality:

  1. It makes a state of the art error handling (and recovery, which many exception-based systems can't really do in practice) system look like technology that's at this point 25 years old and
  2. It's not clarifying anything. Because such terminology references back to exceptions, it only confuses the matter.
  3. It's just straight-up ugly on all of the syntactic, the semantic and the conceptual level. If it even can be made pretty, I've seen no attempts at that either.
4 Likes

Exceptions, as in "try...catch ... throw.. etc" are as abominable as GOTO. They make incomprehensible spaghetti out of your programs.

My basic objection is that there are no such things as "exceptions". Apart from those that cause total program failure. Like running out of memory or time. In which case the only "exception" you need is to die immediately.

Sure it's nice to think about a sequence of operations like "open a file", "read some stuff", "process it", "do whatever". Whilst not having to think about any failures of those steps. Farming out the failures to some other parallel world of execution.

In reality files fail to open, reading stuff fails, all the time. Everything fails, all the time. Failure is a natural state of being. Deal with it in your code. Don't pretend it never happens and punt thinking about it to some mythical exception handler out there somewhere.

4 Likes