Rust is like java

we had this funny realization:

<Soni> do you know how ppl use checked exceptions?
<Soni> (are you familiar with how ppl actually use them?)
<Soni> so, there are 2 main ways ppl usually use them: the try { } catch (IOException e) { throw new RuntimeException(e); }, and the throws Throwable
<Soni> in Rust, these are also known as unwrap()-everywhere and docs.rs/anyhow

and it hurts and we wanted to share. anyway.

2 Likes

Error handling has three main cases:

  • "Internal": This should never happen, don't complicate the API and just die: unwrap() / RuntimeException
  • "Library": This is something the caller should care about and handle specifically: enum Error / throws SomeError
  • "Application": This can happen but there's not terribly much the caller can do to fix it so it's not worth specifically handling, just print and exit or whatever makes sense for the what the caller is doing: anyhow / throws Throwable

I will say that really good "library" errors are by far the hardest; rust does make them nicer than my experience in other languages, but it's still relative. If you're not actually writing a library though, it's fine, just toss in anyhow until you actually need something more!

21 Likes

we think exceptions and error handling are very misunderstood, both in rust and in java.

(and in every other programming language too.)

but it's funny that rust effectively embraces the singlehandedly worst feature of java.

Errors in Rust have parallels because there's underlying reasons for those parallels, that's the point of my previous reply? Technically there's not much in common at all, and they got there from very different histories.

Please do enlighten us. I'd love to know what we are missing.

But but, Rust does not have exceptions. It has panic! but I think that is something entirely different.

checked exceptions are functionally the same as Result. the developer experience is the exact same (tho the syntax is a little more verbose for checked exceptions vs Result).

propagating them? as easy as adding throws. handling them? as easy as catching them. unwrapping them? convert them into unchecked exceptions. and, like Result, the compiler forces you to deal with it.

however:

there's no real-world programming language where you define the exceptions in your API contract, and then those are the only exceptions you care about in the body of your function.

rust forces you to think about all the errors from your libraries. python forces you to... not think about them? (it doesn't have checked exceptions) java has a mix of both, due to mixing of checked and unchecked exceptions.

indeed, if you have an impl Read for YourType, you actually need to turn everything into io::Error! (or unwrap it.) isn't that a little ridiculous? (and indeed this is also a problem with checked exceptions. it's the same problem!)

instead, what if there was a way to say "this is the API contract, these are the only things the function should be responsible for handling and/or propagating". everything else passes through as-is because it's irrelevant to the API contract anyway, but you, as the implementer (not the caller) has to handle API-relevant exceptions from any libraries/etc you use in your implementation. this avoids the problem of putting errors inside errors (unless you like your 1GB error structs for some reason), this reduces cognitive overhead, and this still sidesteps checked exceptions.

in fact... these "this is the API contract" blocks don't even need to be in the function signature, since they're purely internal to the function. alas we don't think anything similar to this is possible in rust today, so we can't give an example of what it'd look like, but trust us when we say it's really all the benefits of unchecked exceptions with none of the drawbacks! and, since this unlocks the true power of exceptions, you might even call it exception-oriented programming.

1 Like

This is only true for some instances of functions that return Result. You also have functions like Rc::try_unwrap that aren’t really exception-like at all— Instead, they use the Err variant to return the input back to you if the operation couldn’t be completed.

1 Like

oh sure.

they could do the same thing with checked exceptions tho.

(in fact that might be the only good use of checked exceptions)

Makes me wonder if Rust would have chosen an Either return type for some of those if it had kept Either part of the standard library.

(Similar to how there’s still sometimes the choice between -> Option<T> vs -> Result<T, CustomError> with a unit-struct CustomError type.)

1 Like

Could you elaborate on this? Are you saying that error handling should be implemented more or less strict depending on the type of software?

Wouldn't you in an API also desire nice error handling with different error types like io, parsing-errors, and even basic HTTP status codes like 409 for duplicates? So the user of the API knows what went wrong?

Not disagreeing, just wondering.

Results are different in that they force you to handle the error when they occur, you cannot handle all errors once. A small but (positively IMHO) important difference.

Overall I think they are similar but somehow Rust developers seem to better handle Results than Java developers handle checked exceptions. Don't know the reason though.

2 Likes

I like that idea, and the world does need new ideas for error handling.

That's the major drawback exception error handling has, and would be a major step back for Rust.

Having errors as part of the function signature is not to increase the cognitive load. On the contrary, it's precisely because there's nothing hidden from us as developers that we can better reason about the code (let alone making it easier to perform static analysis) and its intended behaviour.

If there's anything to change (improve) about Rust's error handling, I think it would be about panics.

6 Likes

This seems contradictory? If the API contract is that you need to handle FileNotFound, that by definition is not purely internal.

You can model this in Rust with something like anyhow::Result<my::Result>, but I would appreciate an ergonomic Result::try_map_err or something. Having a match with a fallback result @ Err(_) => result? or Err(error) => return Err(error.into()) isn't so bad though...


More that the type of error contract depends on what the called code knows about how the calling code will use it - like any API really, but errors tend to fall into those three buckets, and most often "library" errors lines up with them being part of a public library crate, which is unlikely to know if the caller will want to handle different types, hence the naming.

if the API contract is that it raises FileNotFound when the desired file is not found, then it shouldn't raise FileNotFound when the config file is not found.

the latter would, in fact, be a hard-to-trace bug. because it would be indistinguishable from the desired file not being found.

Ok, so don't do that? I'm not sure what you're actually trying to argue here is my point.

what if a language construct just did that for you

Did... what? I'm saying I don't think I understand the feature or functionality you're asking for here;

What it sounds like you're asking for is something like negative throws clause, where you declare which exceptions you won't throw. But you could then just ... catch them? Or in Rust, match on them?

This probably just means I simply don't understand what you're actually asking for; perhaps a pseudocode example might help?

I may also misunderstand. But I think the idea is to declare that certain errors in the fn body should be automatically handled by converting them to some sort of implicit runtime error, that can be specially caught at any level higher in the call stack. Any example would be the std:io::Error returned from a std::io::Cursor, since these should never occur.

This is a nice idea, and something I’ve thought about a little bit before. I think with a correctly-implemented effects system it would be possible to do this. I even wrote a whole blog post about it:

https://claytonwramsey.com/blog/effect-polymorphism

2 Likes