In Java, unchecked exceptions are similar to Rust's panics. Sure your less inclined to catch panics than you might be to catching unchecked exceptions, but you can do it in either. Though perhaps Java's Error is closer to a rust panic.
Checked exceptions are just like the Result type IMO. The discoverability of the error surface is the same, you just don't get the nice pattern matching and sum types to handle/represent it.
What I'm getting at is that I'm a fan of the way Rust does error handling, but I don't get _why_ it feels different to the way Java does it. On the surface they're the same, but I loathe the Java approach. Maybe it's just whatever the opposite of rose-tinted glasses is.
It's because of the way the exception information gets annotated. In Rust, exception information is part of the return type, so you can handle potentially-exceptional functions using mechanisms that are generic over any return type. For example, Iterator::map lets you call every function that might fail, and gather the results as failures, even if you don't fail-fast. But in Java, the exception information is passed in a separate part of the type signature, so generic functions have to be written differently to also handle functions that might fail.
With a result type, in most languages, you can't just ignore failure. You must handle it, if you want to access the value. This prevents the common Java bug where a checked exception is caught but not handled. In rust, you must handle the error, propagate the error, or panic and fail fatally.
Results don't abuse a runtime either. The exception process is slow - stack traces must be generated and the runtime itself needs to manually find the catch block responsible. Returning a result doesn't find this problem.
Checked exceptions are fine for exceptional cases. At risk of sounding facetious, that's why they're called exceptions.
But there's an important distinction that we need to make: logic failures are not exceptional.
If you expect an operation may produce an error, you should handle it or crash the program. You should do this without incurring a performance hit, and using reliable and transparent primitives. Result types achieve both of these criteria, checked exceptions neither.
The drawback to results is mainly the loss of data. Generating a stack trace is expensive, but often that's a worthwhile cost. Exceptions could be more versatile, in that any part of your code can throw any type of exception, but a result can only contain a specific type of error. That's not always a pro, though.
So in general, checked exceptions are pretty awful for expressing normal failure cases, such as not finding a record in a database. They aren't great for anticipated errors, such as not finding a file, because of the chance a programmer might allow this to silently fail. Runtime exceptions are highly valuable, however.
[1] - A decent SO post on the topic: https://stackoverflow.com/questions/613954/the-case-against-...
How does the Rust way avoid this? You can just as easily match on an error type with a catch-all case and ignore the error.
MyType x = null;
try {
x = doRiskyThing();
x.doThing();
} catch(CheckedException e) { ... }
x.doThing();
This code makes it unclear where the exception is sourced from, and makes it seem like you can use `x` even in a failure scenario. In Rust, however: match doRiskyThing() {
Ok(x) => x.doThing(),
Err(e) => ...
}
Here, it's clear where `x` is available and valid. You could even have the error path panic and merely return `x` if you wanted to use it later on in the method (borrow rules permitted, etc.)A model that fits this more closely in Java is 'try-with-resources'.
You can still use Java's type system to get something approximating Rust's approach. One upshot of Result-based error handling is that you can often replicate it in any language by shaving off one or two of the biggest benefits to suit that language's constraints.
let x = doRiskyThing();
match x {
Ok(x) => x.doThing(),
Err(e) => ...
}
x.get().doThing();
That would be more comparable to the try catch from above, and a comparable try catch that actually is semantically approximate would be: try {
MyType x = doRiskyThing();
x.doThing();
} catch(CheckedException e) { ... }
Putting these two together, it makes the Result type seem worse than a try catch, right? But that isn't fair to result, because I'm using it like a jackass.In other words, you can be a jackass using either of them, but you can also use them correctly, and when you use them correctly I don't see how they're any different. I also don't feel like the likelihood of using a Result correctly (edit: originally this said _incorrectly_) is substantially higher than a try/catch, but it probably boils down to developer experience and comfort with a particular varietal and not the capabilities inherent in either. Probably. And if not, I'm looking forward to being shown why! :)
let x = doRiskyThing();
match x { // Pattern match on x
Ok(y) => y.doThing(), // Giving the result a different name clarifies what's happening
Err(e) => ...
}
x.unwrap().doThing(); // You can assume it's Ok, but you explicitly risk the application panicking
Of note, Result::unwrap is approximately the following: match x {
Ok(y) => y
Err(_) => panic!()
}
x is still a valid value for the duration, and is never uninitialized; and whether you handle each case in a match or unwrap, both success and failure are considered and responded to before moving on. The Java checked exception code does that as well, except that it is unclear whether `doRiskyThing()` is throwing the exception, or `x.doThing()`; it's hard to reason about where the error originates from, and how much work was partially completed. In Java, the best way to circumvent this is - if possible - to keep try-clauses as tight as possible.You can only do it with unchecked exceptions in Java. In Rust, you can transparently do it with iterators and the result type.
So given xs: [A] and f: A -> Result<A, E>, it is trivial to typesafely get Result<[A], E> by xs.map(f) where map: forall T. [T] -> (T -> S) -> [S]. This is outright impossible with Java. You have to circumvent the type system, or emulate Rust's approach.
Is it the fact you would put your xs.map call in a mapper or something?
Because I know for sure you can do it in Kotlin: https://github.com/michaelbull/kotlin-result
But I don't see reified generics as a requirement for what you describe, and that's the main Kotlin-only feature I see being used
a) I asked how is it not possible in Java. The Kotlin library is just an example of something I use, it's not relying anything impossible in Java as far as I can tell... like I said reified generics don't change what the calls would look like, just what the implementation looks like
b) What do checked exceptions have using Result? The whole point is you use Result monads in all your code instead of exceptions. When interfacing with legacy code you wrap any exceptions in a Result class as well.
You can emulate Rust's approach with Java, by creating what is essentially a sum type like Result. You'd have to enforce that any access to its content must also handle the error case, and I don't really know how to do that generally. There are various hacks, like having a bespoke sum type for that particular operation that twrows a particular exception on access to its content. But that gets really old, really fast.
And if there are multiple places within the try block where the caught checked exception can be thrown, then basically the catch block can do nothing sensible with respect to recovery or even guaranteeing anything about the current state of the world once you're in the catch block.
I've seen some horribly ugly patterns used in an attempt to deal with this - basically you need to explicitly record progress within the try block which can be tested in the catch block.
And whatever about Java - at least the design decision hasn't completely poisoned the subsequent development of the language. Whenever I use C++, it feels like a massive amount of the complexity, wizardry and arcane/complex patterns required to write "safe" C++ would not be necessary if they hadn't added exceptions to the language early on.