* Java also has unchecked exceptions, and, incredibly, there's still no consensus as to which exceptions should be used for what and when, though these days the most common approach is to simply stick only to RuntimeExceptions, which is terrible.
* Functional error handling using Option, Either, Try etc are arguably much simpler, safer and more powerful than exceptions.
Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.
Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)
Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.
It could be that, when learning Java, Python and any other language, we learn that methods return objects... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.
When lists and booleans also implement map, flatMap etc, you can actually reduce syntax of languages even further- there's no need for looping syntax like for, while etc, and no need for if else either. This is probably too extreme for most people, but think about it!
Maybe flatMap would return true only if all chained booleans are true?
Bimap would allow you to provide functions for what to do in true and false case?
Fold would allow you to turn true or false into a single type?
I'm sure someone more experienced in FP can weigh in with something more correct! :p
map :: Bool -> (() -> b) -> Maybe b
map True f = Just (f ())
map False _ = Nothing
Or, in Haskell because you've got laziness you could go with: map :: Bool -> b -> Maybe b
Which already exists as a combination of Control.Monad.guard and Data.Functor.(<$). map tf b = b <$ guard tfIn fact I'd say checked exceptions are a richer formalism since they essentially provide something akin to intersection types for errors.
The fact that there are also RuntimeExceptions comes up a lot as an argument against Java/checked exceptions but that doesn't convince me. Haskell also has runtime errors that can happen (division by 0) that are outside the Either or Option/Maybe world. The problem is maybe rather that people are lazy and the trend became to decide that everything should be a RuntimeException because who needs all that noise and checked exception handling, right? I'm fairly certain that people who rejected checked exceptions would also reject the more functional approaches.
My argument is that no consensus around using checked exceptions correctly ever developed and so they never really had a chance to shine.
Having said that, sure I think having more usage of idioms we find in FP for error handling/propagation or early aborting is great. It's better than just raising runtime exceptions. However we are still in a world where no widely known language actually enforces (!) error handling in that fashion. It's all convention in the end.
And so my point was that if you want a language that tries to encourage error handling, with known failure modes communicates in the method signature then Java's checked exceptions sound pretty close.
Sure they are not perfect but the question is whether it's worth exploring better formalisms along the lines of first class errors with dedicates language support or piggybacking on the existing type system features and introduce conventions? The latter sort if works but I find it less ...exciting.
So this means, you will have a whole bunch of re-throws, or exceptions wrapped in "higher design level" exceptions which you then throw to finally communicate the error condition to the proper component, polluting all signatures on the way with "throws".
Or you catch it as early as possible, wrap it in a RuntimeException, re-throw it unchecked and let the high level component properly handle it, effectively bypassing the "checked exceptions" system :D
Just my two cents, I know this might be a controversial position.
For instance, Java's checked exceptions break higher-order functions. A map function should be able to say "I can throw anything my argument can throw". Some wrapper functions might say "anything my argument can throw, minus FooException because I catch it, plus BarException because I might raise that myself."
If we're going to dig into it, first we need to call out the occasional need to interrupt running pure code (eg. "the user hit ctrl-c"). As it can happen at any point, I think it's uncontroversial to say that return values are inappropriate? These are Haskell's "asynchronous exceptions", I think they do their job fairly well, and I don't know of a compelling exception-free alternative (although I'd be very interested to learn).
So now we can restrict ourselves to "synchronous exceptions" - those raised because of the function we've called.
I agree that we don't need them. They bring to the table early termination and transparent error propagation. There are contexts where these are desirable - if you disagree on that point let me know and we can dig in. In sufficiently expressive languages, we can do it ourselves - so that's plenty to show we don't need them.
But we may want them.
When we do it ourselves, we usually lack a way of telling the compiler which paths to privilege, and we often find some overhead compared with native exceptions.
When exceptions are provided in the language, we get special syntax to distinguish them. You describe this as a drawback, but nonuniform syntax can help with legibility - if different things look different, I can more easily figure out what I'm looking at. Obviously there are tradeoffs here.
Putting exceptions in the language also provides a distinction to the compiler in type checking, allowing different rules to apply. For instance, Java methods throw some set of exceptions, which get unioned (&c) in ways you can't speak about sets of types otherwise. Simply applying the ordinary value-level rules would be problematic (I hit this in Haskell sometimes). Ideally you can broaden your type rules to support what you need for all of it in one system, but it's not always clear they combine well. Even if it all can be made to fit, I could easily see a situation where assumptions need to be made for inference to work, and the most ergonomic assumptions are different for most values vs. bags of errors.
It's just so much better and easier to understand than exceptions! Doesn't require any special syntax or mental gymnastics to understand the execution path either.
Say you have a function f1 returning an Either a b value then another method f2 which calls f1 but doesn't handle the error case will simply flatMap or bind or whatever the language of choice is calling it and return yet another Either a b value until at some point up the call hierarchy someone actually decides to unwrap and if necessary do something with the error case. The function signatures have the same problem as the parent comment mentioned.
flatMapping over an Either and returning the result is no different than adding "throws SomeException".
Then Option types (since they were mentioned as well somewhere) are a bit of a different story all together. They make sense for describing absence/presence if values/results and are definitely useful. However they don't compete with Exceptions in any real way since they don't really work for error handling beyond the most basic cases (i.e. where handling the error somewhere doesn't matter/need to happen).
stream.map(f).collect(…)
is where checked exceptions failed. #map is a generic method that should obviously throw each of the checked exceptions f can throw, but without some kind of generic sum type Java can't express that. Everyone is forced to choose between "can throw any exception" (e.g., Callable<T>) and "can only throw unchecked exceptions" (e.g., Function<T, U>), each of which spreads virally and punishes anyone using specific throws clauses.You need generics for exceptions. If the inner function can throw some exception, then it should be possible to declare the stream method that it will throw the same exception.
I believe that the JVM doesn't actually support this, so the reasons were got the horrible situation we're in is because they didn't want to sacrifice backward compatibility.
The problem you describe may be an issue with checked exceptions but it would equally be one in any language with the aforementioned feature.
> We're not going to handle each of those cases differently, if we were going to we could always catch the specific exception type explicitly.
But you don't have to handle them differently. You can though and your method signatures will tell you what type of errors you might have to deal with. Some make sense to handle on the spot others don't.
You don't get that with runtime only exceptions. With those you really don't know and can get all kinds of errors and so you expect all and then just catch all and deal with it or blow up/abort whatever is going on (which let's be honest is what most web services do). Is that good? Yeah, sometimes. It certainly is easier to do that instead of thinking about possible failure modes when you know you'll only end up logging the error and give up anyway. I'm not trying to be cheeky, it really is overkill to make full use of checked exception in some cases.
Granted I haven't seen many great examples of checked exception usages so far but I wouldn't dismiss them because they don't mesh with what you're used to or with what's common in a given domain.
And with my .NET hat, I hate having to dig into outdated documentation to discover what exceptions might come my way, so one ends up catching any kind of exception, just in case it might blow, in critical code paths.