It doesn't help that there's no way to specify in the contract of the function that it can throw. Blowing up your entire program because it can't parse a URL is something you should be made explicitly aware of.
Why? I never understood the objection to checked exceptions. It seems to me they would give me exactly what I want! (Granted, I'm a Haskell programmer, not a Java programmer.)
On top of all the other problems pointed out, java's checked exceptions don't even do a good job of indicating possible failure conditions. The standard library set the tone on this with exception like IOException, which has an enormous amount of subclasses represting different IOExceptions, and is thrown by anything related to IO. So I still need to rely on documenation or code inspection to understand what exceptions might actually be thrown and what they mean, if I want to recover from errors.
The classic case of this is that the compiler cannot tell that
But, you're conflating a language feature "Java's checked exceptions" with bad API design, which is obviously possible with any combination of any language's features. I agree that Java's standard library has a ton of bad APIs--not just the checked exception hierarchies, but that does not necessarily mean that checked exceptions are a bad idea.
Just to put an extremely fine point on it, one thing I always complain about is the JDBC ResultSet API. If I have a SQL database table that has a nullable integer column, and I query that column for a row that happens to have that column set to `NULL`, then the ResultSet API will give me the int value `0` for that column and I have to just know that I need to call `wasNull()` (or whatever it's called) to ask the ResultSet if the last thing it returned was really supposed to be null or not. Yet, the ResultSet API does not make me think that Java shouldn't have `int`, or class methods, or any way to query databases--it only makes me think that the JDBC API is really, really, poor.
The question was, why checked exceptions saw no love. A big part of the answer is that the stdlib did a very poor job showcasing them.
No wonder, it was an attempt to graft an FP-esque feature onto a deeply imperative OOP language, with a development team aligned with OOP concepts. I suspect the stdlib team lacked either the understanding or the means to make the use of e.g. IOException nicer. They didn't even have type parameters at their disposal yet. It was botched as a result, and deprecated.
It's an interesting mental exercise to try and design a better implementation of the idea within the constraints of Java. (Implementing the monadic approach is likely the easiest and cleanest way.)
> The question was, why checked exceptions saw no love. A big part of the answer is that the stdlib did a very poor job showcasing them.
Fair enough. I can see that interpretation. I was more focused on their first sentence, which said: "On top of all the other problems pointed out, java's checked exceptions don't even do a good job of indicating possible failure conditions." Then it felt like they were backing that claim by citing a specific example that's just a poor use of the feature.
> No wonder, it was an attempt to graft an FP-esque feature onto a deeply imperative OOP language, with a development team aligned with OOP concepts.
That's an interesting take that I hadn't really heard before or considered myself. I had assumed that it came from some frustration around C++ exceptions and not knowing what was supposed to be handled vs not.
But, in any case, I do agree that the feature ended up rejected by the larger programming community largely because of Java's specific implementation of it.
Though, I have to say that I think it might have ended up rejected even if Java did a perfect job of it in the standard library. The truth is that most programmers don't seem to understand the intended design of the feature and feel like there's a binary choice between using checked exceptions or unchecked exceptions. The Oracle documentation does a very good job, IMO, of explaining the feature, how it's intended to be used, and how do decide whether to use an unchecked exception or a checked one. But, I don't think most people actually read manuals/books to learn programming languages anymore.
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.
My take is more that functional style really wants everything to be a value. And checked exceptions break that pretty heavily. Same reason most functional style heavy languages don't have statements as much as they do expressions.
For places it gets really intrusive, consider that when/where an exception is raised in a map statement.
There is also the annoyance of how junior programmers will fixate on trying to model every exception thinking that means they have handled them. With many exceptions being necessarily delivered all the way to the user, granular modeling of them is of rapidly diminishing value compared to quickly getting it to the user in the first place.
Without parametric types, your methods grew longer and longer lists in the "throws" clause. It was easy to miss one of the dozen exceptions and only have the compiler point you at it after wasting several minutes on compilation (think 133MHz CPUs and slow HDDs).
Type inference in modern Java makes you forget how long and repetitive type signatures used to be in it 20 years ago.
BTW this makes Result<D, E> more ergonomic anyway: you still have only one E, at least one per invocation level. With checked exceptions, you had to haul a whole garland.
One of the problems is that return types and exception types are not unified so it makes composition harder. `Result` brings exceptions into the regular type system
Thanks to everyone who replied. I'm responding to my top-level comment rather than to everyone individually.
It sounds like the objection is not to the fact that the exception type is checked, but that "checkedness" doesn't fit in well with other aspects of the language. For example, you can't be polymorphic over it, or abstract over it.
I'll let you in on my cynical take. Warning: it's both cynical and a bit condescending.
The truth is that programmers are just as susceptible to fads, cargo cults, and appeals to authority as anyone else. Around the same time that everyone decided that checked exceptions were horrible, programmers were also deciding that they didn't like static typing at all and a great many were jumping to Python and JavaScript for everything.
Now, the pendulum has swung the other way, and everyone likes static typing, and they'll even go so far as to proclaim their love for Rust's Result type, which is the same damn thing as checked exceptions in practice (with extremely similar pros and cons and ergonomics). But, they haven't yet gotten around to questioning their belief that checked exceptions are axiomatically bad, so you'll see people point out every minute, trivial, difference between Java's checked exceptions and Rust's Result type in an attempt to rationalize why they like one and not the other. Some will also claim that Rust doesn't have unchecked exceptions, just because you can set a compile-time option to abort on what Rust calls "panics".
Checked exceptions in Java are not first-class as values or types. They don't behave like anything else in the language and you can't meaningfully abstract over them. Combine this with a bunch of boilerplate needed to handle them and you have unescapably painful APIs.
> I never understood the objection to checked exceptions.
Any non-trivial code tree will throw an unmanageable number of exceptions. The solution, in Java, is to catch exceptions and rethrow them as a new exception with the previous exception squirrelled away untyped within it. So either way, it's a failure.
It also doesn't work with higher-order functions.
It's also, in my opinion, conflicts with polymorphism. If you have a interface that can be implemented many different ways, you can't possibly have correctly typed checked exceptions for it.
Disagree. Semantically if you are wrapping an error it means you that you are purposely shifting the responsibility and declaring that your function is responsible for the failure. If you instead re-throw the error, such as if was an IOException, semantically it means that you are not responsible for the error and that the calling code should deal it. If later on you need to add a new thrown exception, it does result in a breaking change on upgrade but it should, as new errors should be thoughtfully handled.
The real culprit is that most people realize that they are not building rockets and thus think that errors, recovery from error and mitigating data loss are not worth their time. Just because I’m not writing a rocket schematic does not mean it’s okay that you lose my 2 hours of work, or that PagerDuty alerts go off at 2am because there was one blip and the database is now slightly corrupted, or you put my account into an undefined state where I have to beg support for help. If errors are checked, many people just wrap all errors will-nilly. If errors are not checked, they never bother with sensible error handling.
To me, whether a language has checked errors or not is just feature of the tool, and whether someone uses tools correctly or not is up to the user.
> Semantically if you are wrapping an error it means you that you are purposely shifting the responsibility and declaring that your function is responsible for the failure.
I don't understand what this means. If you wrap an exception and throw it, how has your function become responsible for the failure?
> If errors are not checked, they never bother with sensible error handling.
My most robust desktop application has a single exception handler at the event loop that puts the exception message in a messagebox. Try and save your file to a network location that doesn't exist anymore? Click OK and try again.
> To me, whether a language has checked errors or not is just feature of the tool
You didn't address the criticism that checked exceptions are fundamentally incompatible with other features such as higher order functions and polymorphism. It's an experiment that has ultimately failed in practice.
> I don't understand what this means. If you wrap an exception and throw it, how has your function become responsible for the failure?
That’s a declaration that this specific kind of problem (say, an IOException) can be handled as this other, higher-level exception (say, of type MyApplicationException, that when bubbles up, will display a friendly error dialog to the user, and logs the underlying IOException for the devs).
> checked exceptions are fundamentally incompatible with [..] higher order functions and polymorphism
They aren’t. Look into languages with algebraic effects.
Maybe I know how to handle IOException but not MyApplicationException but the method only gives me the latter. There is effectively no handling MyApplicationException so why even force the declaration. Either way most exceptions cannot be handled in any meaningful way anyway so checked exceptions are pointless busywork.
> They aren’t. Look into languages with algebraic effects.
Weak type system made it incompatible with function types in practice, and poor compositional tools made it hard to deal with.
In Rust, you have entire libraries around the ergonomics and management of errors to match different applications. In Java, you are stuck using an error management strategy dictated by the error type itself.
Java is perfectly capable of expressing this thought, it has sum types, so the Result-type road is absolutely available to it. (Optional<T>=Maybe T is part of the JDK even) Although, it might not optimize as well, as this is not common practice.
Not all exceptions are checked. This kind of ruins the whole point. IllegalArgumentException for example is very common and would be what you’d use for a bad input to something like new URL(). So it’s a very common exception to see, maybe second only to the NPE, and yet it won’t be checked. What the fuck is the point then?
No. It’s hated by the majority because it shows a problem which should be handled, but most developers don’t want to care about exception handling, and because of this they make terrible code. They would make bad code even without checked exceptions. Checked exceptions just make it obvious.
However, when high profile people criticise it, they criticise it because, for example, interfaces work terribly with checked exceptions. There are problems with collections for example. Like when filesystem backs a list, then that list cannot throw IOException. This is why there is RuntimeIOException now.
Checked exceptions are only hated because java historically hasn’t provided enough syntactic sugar for it, basing it on inheritance is not the best decision and due to it being sort of outside java’s general type system. (Note: Java is thinking about making switch expressions able to handle exception cases as well, which would help tremendously)
I think the backlash against them was way too hard to the point that basically no other mainstream language dared trying them out, even though exceptions in general are - in my slightly subjective opinion - the best form of error handling, paired with some static guarantees sounds like an optimum. They do the correct thing by default (auto-unwrap in the happy case, auto-bubble up with a stacktrace in the unhappy one), and let’s one control their handling on as wide or narrow scope as required (try blocks). Nonetheless, I think that they are to be used for exceptional situations, like network interrupts, OS errors, etc. Expected error conditions, like parsing a number failing is best encoded as a sum type, analogously to an Either/Result type.
Newer research languages with effect types might resurface this “lost” idea, I’m definitely looking forward to them.
Swift does this well. People who object to checked errors probably don’t have a good working mental model of error handling, and just want to write lower-quality code that happens to work. Ergonomics are secondary to that in all cases, IMO.
They are hated by some vocal people, who do not like that they have to actually deal with exceptions rather then ignoring them, fingers crossed it wont happen.
I prefer the Optional.of and Optional.ofNullable approach. Analogous there should be URL.of and URL.ofChecked, the latter for potentially unkown input.
Same for Charset.
There is also no way to specify in the contract of the function that it can return null. Blowing up your entire program because it can't parse a URL and returns you a booby-trapped value that blows up your program when you try to use it as an URL is something you should be made explicitly aware of. Right? Although there are docs, of course, but who has time to read them? Or write them, for that matter? For some reason many client programmers are perfectly fine with calling random functions that has names sounding close enough and then expecting those functions to behave the way they expect them to behave, and many library programmers don't bother to document the actual behaviour of their functions because they for some reason believe that descriptive and self-evident (well, self-evident to them) names are enough of documentation.
And you know, I think I can safely bet $5 the way TFA came to be was something like this: the author wrote a code that used URL() with no error recovery-path whatsoever, ran it, and it blew up on that line. The author then rolled its eyes, wrote an (admittedly) ugly try-catch around, and went to write the article complaining about exception.
Now, I can bet another $5 that if URL() returned null what would happen instead would be that the author would write a code that used URL() with no error recovery-path whatsoever, would run it, and it would blow up somewhere down the line, on call to fetch() or something. The author would then ponder the code for several minutes, trying to figure out what exactly fetch() didn't like about the URL it was provided with, find that null came from the call to URL() and add the error recovery there. Crucially, they later would not go and write a article complaining about null returns because they regard it as a completely normal and acceptable behaviour.
This is one of the reasons for the broad popularity of TypeScript, where “[specifying] in the contract of the function that it can return null” is foundational to the experience. Meanwhile thrown errors are unmodelable COMEFROM statements in TS and many other languages.