Especially in more modern Java, a huge problem of checked exceptions is that they are not parametrizable. So you can't (easily) call map(list, foo) if foo throws an unchecked exception, since map() doesn't throw, and there is no way to make map() throw conditionally.
Swift has the concept of conditional throwing for closures/callbacks. The keyword is `rethrows` if anyone wants to do a search and read some docs.
Now, Swift's version of throwing is what I would call "semi-checked", because it does force callers to handle errors from functions that have `throws` in the signature, but it doesn't specify the specific type of error that may be thrown.
But, in any case, that kind of proves that there are other ways to do checked errors/exceptions that are not exactly as Java has done it or the monad-style return value approach that a lot of other languages have done.
I really think the entire static typing programming community threw the baby out with the bath water when "we" collectively decided that checked exceptions were a pariah language feature.
Also, I'm pretty sure that Java's checked exceptions are parameterizable to a large degree. I believe you can write something like (forgive syntax errors),
interface Foo<E extends Exception> {
void doSomething() throws E
}
That doesn't fix the issue that you're specifically addressing, which is that you can't write "this might or might not throw, depending on what you pass to it", but it's more than I think many people realize they can do.
And, one more point: I don't think it's actually a problem that the Stream methods don't allow for throwing. A "map" from one type to another is not really supposed to be fallible--it's not a "mapping" if that's the case. That's a case where an imperative loop is more semantically appropriate, IMO.
Thanks for the swift pointer, I’m not too familiar with that language.
As for the more formalized version, effect types come into the picture, that let a function be parametric in how it handles its functional parameters. E.g. a `map` could be throwing if given a throwing f, but non-throwing otherwise. But these can even handle more complicated effects as well.
This is the vast majority of the problem. As a consequence, APIs with callbacks or wrapping generic things of any kind are forced to choose between "throws Exception" and a runtime exception, losing all detail and possibly choosing the opposite of what is actually needed. And there are TONS of these, and they're heavily used.
It's a badly crippled implementation that has poisoned the well, the concept is just fine. People like Result<T,E> well enough, and that's exactly as expressive as checked exceptions in languages that implement them correctly.
while `Result<T,E>` is very similar to an exception there are a couple of limitations:
1. results must be handled explicitly, exceptions are forwarded automatically. This means I must deal with each result locally, even with a "forward up" to the caller. This means much complex and less readable code
2. exceptions have a stacktrace, handled automatically for you. This means more precise location of where the error occurred. For the same reasons exceptions are heavier - as they keep more data AND is updated for each frame (when handled, and depends on the implementations)
3. the syntax to deal with exceptions is ugly as hell. I haven't found anything better than the python syntax, and still is not something you like to see.
> 1. results must be handled explicitly, exceptions are forwarded automatically. This means I must deal with each result locally, even with a "forward up" to the caller. This means much complex and less readable code
Rust address this very concisely.
In Rust, "forward up if error" is one character, '?', and you can't forget to use it because it also unwraps the non-error value.
In Go on the other hand, "foward up if error" is a notoriously repetitive multi-line sequence. Some people like it because it forces every step of error forwarding to be explicit and verbose. Some people don't.
In Go I've worked with, there's a lot of these. People aren't avoiding error handling to save keystrokes. But occasionally I've found a mistake in Go error-forwarding that went unnoticed for years and would have been detected by Rust-style statically-typed Result, or any kind of exceptions, so I'm not convinced the Go-style explicitness really helps avoid error-handling bugs.
> 2. exceptions have a stacktrace, handled automatically for you. This means more precise location of where the error occurred. For the same reasons exceptions are heavier - as they keep more data AND is updated for each frame (when handled, and depends on the implementations)
That's true in practice, but it's a design choice, not a strictly required difference between exceptions and Result<T,E>.
In principle, the heaviness of a stacktrace can be eliminated in many cases, if the run-time (with compiler help) keeps track of whether an exception's catch handler is never going to use the stacktrace (transitively if the handler will rethrow).
Lazy, on-demand stacktraces are also possible, saved one frame at a time on error-return paths, and sharing prefixes when those have been generated already. These have the same visible behaviour as normal stacktraces, but are much more efficient when many exceptions are thrown and caught in such a way that the run-time can't prove when to omit them in advance.
In principle, the same things can be applied to Result<T,E> types: Take a stacktrace where the Result is constructed, and use the above mechanisms to keep it efficient when the stacktrace isn't used. In Rust, there are library error types that save a stacktrace, if you turn the feature on, but I don't think any of them automate their efficient removal when unneeded, so turning the feature on slows programs unnecessarily.
I agree that, in principle, Rust's mechanism could be the best of all worlds. Unfortunately, the fact that ? doesn't add any kind of context makes it a non-starter from my point of view. There's nothing worse than a log saying "Failed to perform $complex_multi_step_operation: connection failed". Even Go's ugly verbose error handling pattern is better than building up no context at all.
I don't really understand why the Rust designers didn't feel that adding context to errors would have been a much better built-in macro than just bubbling up the error with 0 info about the path.
tbh the quality of default information (and lack of ability to ignore by accident) is why I'm mostly a fan of exceptions. In practice more than in principle. The vast majority of code just doesn't add enough context when it has to be done by hand.
What I think I really want is Rust-like with compiler-added return-traces by default, unless explicitly opted out (in code or in build time config), and you can also add extra info if you want.
Yes, I'm in exactly the same boat. In practice, in the code bases I worked in, the ones with exceptions tended to have the most useful debug information available in logs.
It's absolutely possible to beat exceptions with manual context, but I've only really seen that in areas of code that were actually causing problems (so people actually put in the work to make the logs useful). When a problem appears in production in an area of code that had not been causing problems in QA is when you're typically left with no useful info in logs.
> forced to choose between "throws Exception" and a runtime exception,
Not that it fully defangs the issue, but there are two other options that come to mind.
1) suppress the exception (maybe with a log)
2) capture the fact (and maybe content) of the exception and expose it through the API in some other way
Probably neither of these is going to be appropriate for most cases, but seem worth surfacing because 1 is often chosen even when it's inappropriate and 2 is often overlooked as an option.
I do not see, how this is a _huge_ problem, though. Maybe a nuisance. I tend to lay out my map function anyways, as i have to test those. And there i can have my error handling right away and return an optional or a record imitating maybe/eitheror. Annoyed i was with IOexception, from which i can not recover anyways, but this has long been addressed by introducing UncheckedIOException.
I was very happy with Java-the-language as well, back when I worked with it. Calling this "a huge problem" is of course an exaggeration - but it is very much a regular annoyance and it is one almost inherent to checked exceptions.
I also don't agree that IOException should not have been checked. If there is any purpose for checked exceptions at all, than IOException is the most important one to check. It is the quintessential example of a good use of exceptions. It is also the one for which it most likely that the code can automatically recover - you can retry the operation, use a fallback file/address/DNS server, you can use a default value if some config file is missing, etc. I don't think there is any other clear catgeory of errors that is more likely to be usefully handled than IOException.