Yeah, I dislike also exceptions. I'm more fan of Rust error handling for example via `Result<T, E>`.
I believe that make a distinction between recoverable and unrecoverable errors is key.
I see a lot of people espousing this viewpoint and I just don't get it. What's the big difference between the way Java does it and the way Rust does it?
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.
As you say, the API feels similar. However, result types have many benefits over exceptions, with a few drawbacks.
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.
I believe the difference is that you resolve the error at the same point where you resolve a successful result, making it impossible for a reader of the function to, for example, assume that you can access a value after an exception was thrown.
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'.
> 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.
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.
Try to stick a function that may fail into a higher order function. The best example would be .map on streams/collections.
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.
The idiomatic error handling mechanism of Java is the checked exception. This mechanism does not work with higher order functions.
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.
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.
With checked exceptions it's not clear how control can pass into the catch block (using Java terminology). The fact that a method can throw a particular exception is not visible at the call site so unless you examine the declaration or definition of every method call within the try block, you cannot be sure from where control can jump to the catch block.
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.
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.