Rust is like java

That sounds maybe better, but I think I'd rather see a trait level solution, if at all possible, something like:

impl Read for Cursor {
  type Error = !;

  ...
}

would let you unconditionally unwrap Ok()s at least.

Serde does this somewhat, but it still eventually has to fall back to D::Error::custom() - perhaps there's now a GATey way to fix that?

And that's a big problem with checked exceptions which doesn't happen as easily with Results, yes: if your function throws FileNotFoundError, then you can call something throwing FileNotFoundError without noticing this, even if this error should not be handled this way (should be converted to RuntimeException instead). With Result, you have to at least add question marks to any fallible call, which allows you to check if this is really what you want.

hmm. well, if you still don't get it, maybe try one of these?

When I read "Exception Oriented Programming" I thought someone was making a joke. Those three words put together describe one of my worst nightmares in programming.

As you seem to be serious and as you say in your blog the syntax in Java becomes horrendous, perhaps you could give us an example of what you mean with the kind of syntax you have in mind?

You keep referring to unwrap(). It's not clear to me why. As far as I am concerned unwrap() should never be used unless one really does not care about total failure at that point. Which could be:

  1. When cooking up some new idea and does not want to be bothered with error handling during development iteration.

  2. One is pretty sure the error is never going to happen and has no idea what to do if it ever did.

To my mind that is the essence of "exception". Something that is almost never expect to happen and one is not equipped to deal with nicely. Like that once in a hundred year hurricane that destroys the entire neighbourhood.

Meanwhile "errors" do not exist. Functions return results. Callers have to deal with those results.

For example: If a user gives a file name to open and that file does not exist that is not really an "error" it is certainly not an "exception". It's a quite normal course of events that should be expected and catered for.

Philosophically I dislike the idea of programming "the happy path" and shunting out anything "unhappy" to some other code, that could be far away, by means of a GOTO called an "exception". Typically those "happy" and "unhappy" paths are just normal occurrences, they are of equal weight and deserve equal standing in ones code. We only need if (or match) to do that, not more heavy weight syntax and semantics.

5 Likes

i wont add anything technically valuable, just an opinion...

Assumption (my understanding of above) is, that there's some "exception" happening only on specific parts of code, that is significant in API/contractual sense. (that is my understanding of the stuff described above)

If it is important enough to potentially require all this special behavior/syntax/whatever, then, from "business logic" point, it might as well deserve own custom Error/Result enum variant in affected module (Rust way of handling things), without any need to "upgrade" the language and add implicit/hidden behavior...

the python thread actually has an example syntax that looks pretty good: Better way to deal with exceptions - #73 by SoniEx2 - Python Help - Discussions on Python.org

in fact there's even a working implementation of this syntax (tho it's much slower than proper language-level support): Better way to deal with exceptions - #74 by jagerber - Python Help - Discussions on Python.org

(it's a bit of language feature misuse, but hey, it works...)

Not being a Pythonista I cannot make heads or tails of that discussion. Perhaps you could give us an example in some Rust like syntax that actually does something, creates errors and handles them. You know fn a() calls fn b() calls fn c() calls fn d() all of which return Result with some error(s) created somewhere down the call chain and handled somewhere else.

I think my final code never contained unwrap() (but often of unwrap_or_something()) [I rarely allow the code to panic, mainly only for things where the error is external and makes the code impossible to run; often the program just enters in a loop to wait for the error to go away while loudly complaining], and I sometimes use my own Error variants to return multiple error sources in one Result (MyError::ConfigNotFound and MyError::DBFileNotFound), so I'm not sure I understand this thread fully.
Also I find Result handling quite nice, Rust providing me various levels of verbosity from unwrap_or_…()s or map_err_…()s up to full featured matches.
But I confess I don't use Java, so I may miss half of the story.

Ok, so the problem you're trying to resolve is that you're in the "library" error case where the caller needs to handle specific error cases, and with Result you need to pass the error up the stack manually, converting the error type reach time, while with exceptions you run into complexity needing to ensure all your meaningful error types are distinct?

When I run into the latter, just ensuring I always use (and document) a fresh error type, eg not FileNotFoundError but ConfigFileNotFound and WidgetFileNotFoundError, or maybe a WidgetParseError with a code saying which, but overall it is a pain.

I would say the former problem just really isn't a big deal in Rust once you get a bit of experience: You just implement From and it disappears into the ?, or add a tuple variant and use result.map_err(Error::Foo).

thiserror among other libraries will make that even easier by automatically implementing From, and chaining to the source error when printed.

5 Likes

no, none of that actually.

in fact we hate the idea of shoving errors inside errors. rust does that a lot, that's how you end up with 1GB error structs.

we just want exceptions, but better. we want to make programmers think with exceptions and we want the language to help them with it. this one thing would singlehandedly improve python and java and other languages that use unchecked exceptions, yet nobody's advocating for it because "oh result is better you just shouldn't bother with exceptions". (yet, we disagree that result is better, clearly, or else we wouldn't be bringing this up in the first place.)

being able to throw your own exceptions across someone else's API is great. having to make them think about it is bad. being like rust std and telling the programmer they can't (see: io::Error and the inability to use your own error type) is just plain evil. all of this can be solved with exception-oriented programming.

Do you mean like throwing an exception from a callback?

I would argue they still should have to think about it. Even though they shouldn't handle (i.e. catch) the exception, they still have to anticipate that the execution of their code stops halfway through. If they do not handle that case, it may lead to broken invariants.

For example, that is the single reason Rust's replace_with can not be sound without aborting the program in case of an exception (unwinding panic)!

I cannot imagine how hard one would have to try to get a 1GB error struct. I presume you are exaggerating.

I'm willing to wager that 99% of the "exceptions" you are referring to are not exceptional circumstances at all. Like that file reading example I mentioned above. It is not exceptional for a file not to exist, it's an expected, everyday, common or garden result. Having the absence of a file cause code flow to hyperspace somewhere else seems like a bad idea to me.

"exception-oriented programming" gives me the shivers. Back in the day when they taught "structured programming". There was this notion of "Single Entry, Single Exit". "Single entry" meaning that one a function can only be entered at one place. One cannot call into the middle of it like one can in assembler. "Single exit" meaning that a function should only return to one place. Again in assembler one could return to anywhere. FORTRAN supported alternative returns as well.

The idea being that not following the principals results in a complex spaghetti mess of code that is prone to errors an hard to modify. The same reason we do not like GOTO.

Well, to my mind exceptions break the structured programming rules. When I call a function I have no idea where I will end up when it is done.

Perhaps I'm missing your point. I would love to see an example of what you mean in a Rust like syntax. Then I might get the idea.

4 Likes

Well yes, that's why I said the trade-off with Result is that you have to pass it up the stack? I don't think we disagree on what you're after now, just on the scale of the problem.

Specifically, with Result, that literal passing itself is a literal single character: ?, the reason this does get complicated is the error types in the sort of "sandwich" APIs you're talking about need to be able to represent some arbitrary other error type, but even here it's quite trivial in many cases:

pub trait WidgetConfigProvider {
    type Error;
    fn resolve(&mut self, context: &WidgetContext) -> Result<WidgetConfig, Self::Error>;
}

...

pub enum Error<ConfigError> {
    // Can be io::Error or serde_json::Error or anything else
    Config(ConfigError),
    Load(io::Error),
    Parse(serde_json::Error),
}

...

impl<Config> WidgetFactory<Config>
where Config: WidgetConfigProvider
{
    pub fn make_widget(&mut self)
        -> Result<Widget, Error<Config::Error>>
    {
        ...
        // If it's the only use, otherwise impl From
        let config = self.config.resolve(&context)
            .map_err(Error::Config)?;
        ...
    }
}

So really, the main cost is any types passing the error around need to be aware of all the sources of the error. This is not nothing! Having an error handling approach that allows dynamically testing for and recovering errors from further up is reasonable... and Rust already lets you do that too with using Any in whatever error type you use.

As I said in my first reply in this thread, good library errors are hard in any language, but Rust is better at it than I've found in other languages, as Result and it's related machinery is at least a flexible enough system you can generally find something nice.

2 Likes

we mean, indeed, we can live with it. but we do disagree about rust's approach being good.

besides... isn't io::Error the entire reason the Read/Write/etc traits are unavailable in no_std?

Perhaps, but I'm not sure what the relevance of here? io::Error is fine enough in it's lane, but it's a poor general error handling mechanism. It's overuse as such is an antipattern.

Though it would be nice if you could make the error generic, APIs like io::Read are generally a convenience feature, and you shouldn't use it (as a consumer or producer) if the caller needs to handle different errors. Define your own trait, the caller already needs to be aware of the error types, so it's not much of an additional burden to implement, and you can even blanket implement io::Read in terms of your trait which mostly hides the difference if the caller doesn't care.

if rust had (unchecked-only) exceptions + API contract management features, retrofitting other error types onto Read would be... basically trivial.

Transparent errors regardless of implementation would break Rust completely, but maybe you can get something more like what you're after: what are these contract management features? Like, specifically, what would you expect this to look like in use?

1 Like

exactly like the python example we showed. here's some pseudo-code:

my API contract is raises Foo {
  do some stuff...
  do more stuff...
  this next step might raise Foo as part of my API contract {
    do something that may raise Foo...
  }
  if something bad happened {
    raise Foo too
  }
}

both the explicit raise and the "this next step might raise Foo as part of my API contract" are allowed to raise Foo. the other steps are prevented from raising Foo at runtime - that is, if they do try to raise Foo, the "API contract block" intercepts it and causes another kind of error, like a non-catchable exception - or even an abort.

The ask seems to be isolation of error types that are allowed to occur in specific parts of a function. I admit I have never had such a desire. Isolating part of a function to only return one specific kind of error is done by factoring that part into a new subroutine with its own error type.

fn load_config(path: &Path) -> Result<Config, ConfigError> {
    let json = load_json(path)?;
    Config::try_from(json)
}

fn load_json(path: &Path) -> Result<Json, JsonError> {
    todo!()
}

An alternative interpretation is that error handling should be done inline, rather than bubbling the error to the caller. For example, by loading a default value if a file was missing or there was a parse error.

fn load_config(path: &Path) -> Result<Config, ConfigError> {
    let config = load_json(path).map(Config::try_from).unwrap_or_default();
    Ok(config)
}

Both of these can be done today without hitting the checked exception problem in Java.

We should be careful about over exaggeration, as that can harm the credibility of the claim.

There is no inherent requirement for error types to be nested. The ? operator can perform a pure transformation as errors bubble up the call stack, including ZST to ZST. The reason errors tend to be nested in practice is because error handling is complex. The more contextual information provided with the error, the more likely it is for a human to understand the issue and seek a solution.

I don't feel strongly that a new syntax will greatly improve error handling. And I am unconvinced that there is a problem to solve. Result is nothing like a Java checked exception.

5 Likes

I've mentioned a few ways mixing error types can cause issues and how you traditionally handle them, but you haven't mentioned anything about them other than wanting exceptions instead. But you're writing Rust not Java, you're operating in an ecosystem that has a lot invested in not having exceptions (not just in having an alternative).

In Rust we do that example with types already. If it doesn't return Result<T, Foo> it can't "raise" Foo so the question becomes: what does this problem and solution look like in a world without transparent errors? (Just to be clear, Rust is extremely unlikely m to get transparent error handling, exceptions or otherwise - at least any more than the current panic system anyway, which is not suitable as a control flow mechanism)