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.
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.
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.
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.
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 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.
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.
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.
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.
Nah, i am happy with Java. At least in my domain.
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.