from Hacker News

Checked exceptions: Java’s biggest mistake (2014)

by flying_sheep on 9/11/20, 8:57 AM with 317 comments

  • by alasdair_ on 9/11/20, 4:14 PM

    I like checked exceptions. Yes, sometimes (especially in the oldest APIs when they were still figuring this stuff out), they were overused but mostly I think they encourage developers to really think about what happens in the failure case.

    I notice this especially with less experienced developers and remote calls - a lot of JS code I’ve reviewed in the past assumes the remote call will always work, yet Java code from the same developer will almost always correctly handle the situation, simply because the exception is explicitly required to be handled.

  • by hrgiger on 9/11/20, 3:32 PM

    Well... I will take the bullet and confess that I do like checked exceptions. When they are not miss|over used they transfer the enough required knowledge what is to be handled. You dont need to handle? Transfer to higher levels on stack. That is a great fit in my opinion for applications designed with especially fault tolerance futures and using many external components. Not saying that design of CE is perfect, they might be too broad that doesnt tell you exact handle case, so it will leave you in the dark or in a call chain of a()->b()-c()-d() there might be cases that b and c wouldnt need to have contract in their method signature maybe compiler would decide if exception is orphaned or needs to be handled.
  • by gordaco on 9/11/20, 2:49 PM

    I don't really dislike the concept of checked exceptions, but the awful way they prevent the usage of functional interfaces and modern Java in general is pretty infuriating. Functional interfaces and code that uses them should have a generic way of being transparent to exceptions, but I'm not sure it can be done without breaking the language.

    Nevertheless, I still believe that Java's biggest mistake is not checked exceptions, but the stupid distinction between primitive types and Objects (caused because all the cruft present in the latter would have make basic operations prohibitive in terms of performance, at least for early Java versions) and all the associated boxing. I have seen some extreme cases of performance degradation because of that (fortunately a refactor to use arrays solved the problem, but this is not always possible).

  • by aazaa on 9/11/20, 8:10 PM

    > The biggest argument against “checked” exceptions is that most exceptions can’t be fixed. The simple fact is, we don’t own the code/ subsystem that broke. We can’t see the implementation, we’re not responsible for it, and can’t fix it.

    Here's what Oracle has to say:

    > Here's the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.

    https://docs.oracle.com/javase/tutorial/essential/exceptions...

    - checked exception for recoverable errors

    - unchecked exception for non-recoverable errors

    So the argument that most errors can't be recovered from is _not_ a reason to abandon checked exceptions. It's a reason to reserve checked exceptions for those cases in which recovery is likely.

    The main argument in this article appears to be based on a misunderstanding.

  • by specialist on 9/11/20, 3:04 PM

    How would eliminating checked exceptions mitigate terrible API design?

    I've never understood the angst. All the arguments reduce down to mitigating terrible abstractions.

    The Correct Answer is better APIs. Mostly, that means don't pretend the network fallacies don't exist. Embrace them. Which generally means work closer to the metal.

    I'll say it another way. The problem is EJB, ORMs, Spring, etc. The obfuscation layers.

    Someone smarter than me will have to rebut the functional programming points. I'd just use a proper FP language. Multiparadigm programming is a strong second on the list of stuff you shouldn't do. (Metaprogramming is first.)

  • by Imnimo on 9/11/20, 3:44 PM

    Some checked exceptions make a lot of sense to try to catch and fix. FileNotFoundException is something my code can probably recover from - you asked for a file, it's not there, let me ask for a different file. Having a file reading method declare that it throws FileNotFoundException can be a helpful reminder to make sure you handle that possibility.

    But then there are other types of checked exceptions that are almost certainly unrecoverable because they happen way down in some other third party code. And then you get the endless chain of "throws" all the way back up the code base.

  • by chriswarbo on 9/11/20, 3:11 PM

    I've been using Scala quite heavily recently, having mostly used Haskell for years, with distant memories of Java. Scala allows Java methods to be called, but doesn't bother with checked exceptions, which has bitten me quite a few times.

    My preferred style of error-handling is Option/Either, since I can implement the 'happy path' in small, pure pieces; plug them together with 'flatMap', etc.; then do error handling at the top with a 'fold' or 'match'.

    Exceptions break this approach; but it's easy to wrap problematic calls in 'Try' (where 'Try[T]' is equivalent to 'Either[Throwable, T]').

    The problem is that Scala doesn't tell me when this is needed; it has to be gleaned from the documentation, reading the library source (if available), etc.

    I get that a RuntimeException could happen at any point; but to me the benefit of checked exceptions isn't to say "here's what you need to recover from", it's to say "these are very real possibilities you need to be aware of". In other words checked exceptions have the spirit of 'Either[Err, T]', but lack the polymorphism needed to make useful, generic plumbing. The article actually points this out, complaining that checked exceptions have to be handled/declared through 'all intervening code'; the same can actually be said of 'Option', or 'Either', or 'Try', etc., but the difference is that their 'intervening code' is usually calculated by the higher-order functions provided by Functor, Applicative, Monad, Traverse, etc.

    It's similar to many developer's first experience of Option/Maybe: manually unwrapping them, processing the contents, wrapping up the result, then doing the same for the next step, and so on. It takes a while to grok that we can just map/flatMap each of our steps on to the last (or use 'for/yield', do-notation, etc. if available). It would be nice to have a similar degree of polymorphism for checked exceptions. Until then, I'd still rather have them checked (so I can convert them to a 'Try'), rather than getting no assistance from the compiler at all!

  • by captainmuon on 9/11/20, 7:29 PM

    I think checked exceptions are backwards. If you use a `throws` declaration the caller must catch it. It quickly becomes quite onerous. (Especially if you come from the philosophy that an exception often means "abort this program - unless somebody catches this". In small programs you might just want it to crash early.) And even worse, it is not exhaustive. You can always get a RuntimeException or a NullPointerException from nowhere.

    It would be great if they worked the other way around. Instead of forcing the caller to catch an exception, they would guarantee that no exception leaves a certain block.

    So you would have a function

        void MyFunc() onlythrows IOException {
            first();
            second();
        }
    
    And the compiler would statically guarantee that no other exception can leak out of it - because first and second have been marked `onlythrows IOException` or are "pure" and cannot throw at all.

    For sure you'd need an escape hatch, like Rust's "unsafe". And it would not be very useful around legacy libraries. But it would be tremendously useful if you could drop a block like

       neverthrows NullPointerError { ... }
    
    in your code and be sure that everything inside is null safe! I asked about this a few years ago on StackExchange [1] but so far I never heard about it anywhere else.

    [1] https://softwareengineering.stackexchange.com/questions/3497...

  • by iso8859-1 on 9/11/20, 8:42 PM

    The confusion regarding checked exceptions (which are fine, if not misused, see aazaa's answer) was made much worse by IDE's such as Eclipse, which would generate:

          catch (MyCheckedException e) {
            e.printStackTrace();
          }
    
    This causes unreliable programs, since the programmer will initially only think about the successful path. Eventually, the exception will get thrown, and things will break in weird ways. They may not notice it quickly, because the stack trace will be buried in logs. Alternatively, if the IDE default has been

        throw new RuntimeException(e)
    
    or something similar, which would crash the program, the programmer would have noticed it more easily. Of course, the program would still be broken, but better crash hard and violently than subtly and confusingly.
  • by grey-area on 9/11/20, 3:40 PM

    According to Gosling, including classes/inheritance was his biggest regret.

    I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied.

    https://www.infoworld.com/article/2073649/why-extends-is-evi...

  • by pmcollins on 9/11/20, 3:18 PM

    Possibly unpopular opinion: Java's biggest mistake, by far, was annotations that define behavior at runtime.

    So now we have consultingware like Spring where if something isn't working, it could because you missed an annotation somewhere, or put the right annotation in the wrong place. Which annotation? Where? Maybe you'll find out a week from now that you made a mistake, when a customer finds a bug in production.

    This took all of the compile-time checking goodness that you got from Java and threw it in the garbage. Now you either have to call an expensive consultancy, read books/manuals about your gigantic framework (fun!), go on forums, etc. You can't just use your coding skills.

    I still often use Java for my side projects because I love it without runtime annotations, but thank god for the rise of Golang. I'd rather deliver pizza than go back to the misery that is annotation-driven development in Java.

  • by crehn on 9/11/20, 4:06 PM

    On a slight tangent, I'd rather have only checked exceptions, so I know exactly what might throw where. Instead, I have to rely on potentially outdated Javadoc and debugging runtime exceptions in production to find them.
  • by hodgesrm on 9/11/20, 3:03 PM

    If checked exceptions overall are Java's biggest mistake, then the InterruptedException implementation in particular is the second.

    It conflates thread management with exception handling in a way that's difficult to understand and implement correctly. The relationship between InterruptedException and the Thread.isInterrupted() method is a particular pain point for coders.

  • by josephcsible on 9/11/20, 5:04 PM

    I don't think the whole concept of checked exceptions isn't a mistake, although the way they're implemented certainly is. In my experience, the problems with them almost always stem from one of two issues:

    1. Built-in exceptions that are checked but should be unchecked, IOException being the main offender (I don't mean things like FileNotFoundException; I mean the kind you can get if the OS returns -EIO)

    2. Lack of exception polymorphism, preventing you from doing things like l.stream().filter(SomeClass::somePredicateThatMayThrow), even if the function that you're doing it from can throw the same exception that the predicate can

    I think checked exceptions would be great and nobody would hate them if those two problems were fixed.

  • by kasperni on 9/11/20, 2:53 PM

    This is still one of the pieces written on error handling. http://joeduffyblog.com/2016/02/07/the-error-model/
  • by pjmlp on 9/11/20, 3:00 PM

    I sorely miss them in .NET, specially with libraries without any kind of documentation regarding the errors that they throw.

    Also checked exception haters always overlook the fact that CLU, Modula-3 and C++ did it first.

  • by mumblemumble on 9/11/20, 5:07 PM

    I'm a relative newcomer to Java, and, being a newcomer, I have put some effort into exploring as many corners of the language as I can. One that's proven to be a particular puzzle is checked exceptions. But I think I finally understand them now.

    I quickly found that checked exceptions just do not play nice with any sort of functional-style programming, like the article describes. But the problem goes so much deeper than that. Checked exceptions are also, as far as I can tell, incompatible with an object-oriented programming style. More or less for the same reason that they interact poorly with FP. The fundamental problem is that checked exceptions don't really play nice with polymorphism or higher-order programming of any type.

    Which takes us to the crux of how I understand them now: Checked exceptions may not go well with FP and OOP, but they make all the sense in the world if you're doing procedural programming. There, you're not trying to create deeply nested abstractions, and you're not messing around (much) with polymorphism tricks. The code's very lexically organized, with little in the way of dependency injection or higher-order programming. When you're programming procedurally, it's fine to be exposed to the implementation details of stuff further down on the call graph, because you're the one who put it there in the first place.

    And that, in turn, means that checked exceptions are not really a mistake. They're just a piece of evolutionary history. Because, early on, Java wasn't really an object-oriented language. It was a deeply procedural language with some object-oriented features. It arguably still is, it's just that there's been a big cultural shift toward trying to take a more object-oriented approach since Java 5 came along and made it more practical to do so.

  • by crehn on 9/11/20, 4:15 PM

    While we're talking about terrible decisions, can you guess what the following code will print?

      String s = null;
      switch (s) {
          default:
              System.out.println("Hey");
      }
    
    Hint: it will throw NullPointerException.
  • by jarym on 9/11/20, 3:17 PM

    Mistake maybe but I disagree on the ‘biggest’ part.

    For me type erasure is a bigger issue. I get that it was done for backwards compatibility but the drawbacks imposed by that decision seem to only grow as more time passes and more new compromises have to be made.

  • by imglorp on 9/11/20, 5:13 PM

    I just want to throw a plug for the concept of Railway Oriented Programming. It can be laid on top of almost any functional-ish language that can implement some sort of Result(Ok,Err) return type. And to the OP's point, you can catch exceptions where they occur and `return Result(Err(details))`.

    We applied ROP with great success at a fintech where we wanted to clean up a block of business logic with many failure paths. Instead of a forest of nested conditionals or try/catch mess, there was a very simple happy path with clear handling for all the errors.

    Here's a good start. Ignore the language details, the concept is universal. https://fsharpforfunandprofit.com/rop/

  • by ben7799 on 9/11/20, 3:05 PM

    I've been using java since Pre-1.0 and professionally since 1999 and I don't see checked exceptions as being particularly high up the list of java issues.

    Even with all the functional stuff they almost never seem to really create a major issue.

    My biggest issue with Java is just the way they've caved and constantly added new stuff that is always grafted on so it's never quite as good as a language that focuses on that programming paradigm from the start.

    But none of the problems in the language compare to the scale & scope of the problems caused by Java's default developer & architect culture. The culture is terrible... everything gets overcomplicated, overabstracted, etc. and you've got charismatic charlatans convincing wide swaths of developers to misuse and abuse language features in ways that have made a lot of people hate the language and have produced a lot of buggy and hyper inefficient code.

    Java itself doesn't have to be bloated, slow, buggy, and a massive memory hog. But the java developer community has continually made decisions to structure their java software in a way that makes that the default condition of Java systems. The way the Java language constantly gets new giant features grafted on plays into this.. everyone jumps on the latest language addition and misuses it for a few years before they come to understand it. By the time it's understood there's something new to move onto and abuse.

    Java became everything about C++ it was originally supposed to simplify.

  • by adrianmonk on 9/11/20, 6:53 PM

    There are two kinds of exceptions: (1) those that can (and should) be handled in a meaningful way, and (2) those there's no way to handle and you should just crash.

    A big part of the reason people hate checked exceptions is that actually doing #1 correctly is really damn hard. It's a whole separate dimension of complexity that your design needs to tackle.

    A compiler that checks exceptions forces you to do it. Abruptly, if you're new to the language. It flips the floodlights on at full brightness and makes you see the full scope of the problem. It's tempting to shoot the messenger.

  • by Reason077 on 9/11/20, 4:08 PM

    The biggest mistakes in Java are checked exceptions, NullPointerException, and primitive types. But which one of these is the worst mistake really depends on your perspective, and the mood of the day.
  • by devit on 9/11/20, 4:24 PM

    Rust has a system equivalent to checked exceptions.

    However Rust, unlike Java, has a great macro system and can thus easily generate higher level exceptions wrapping the lower level ones.

  • by nayuki on 9/11/20, 4:53 PM

    In my eyes, Java's biggest mistake is that the byte type is signed instead of unsigned. Masking a signed byte with (b & 0xFF) causes so much needless pain, and I have never wanted to use a signed byte. On the other hand, I appreciate that Java doesn't have unsigned versions of every integer type; that simplifies things a lot. As for checked exceptions, I'm still undecided on whether they're a good or bad thing.
  • by samfisher83 on 9/11/20, 4:27 PM

    I liked checked exceptions. It makes you aware of what exceptions that might happen. Makes you think about how to handle it.
  • by bcrosby95 on 9/11/20, 2:35 PM

    The biggest problem with checked exceptions is that the application writer decides what is a recoverable error. Not the library designer. I could easily imagine a program where SQLException is a recoverable error. But I don't write those types of programs.
  • by spion on 9/12/20, 1:21 AM

    The sad bit about checked exceptions is that everyone compares any sort of error tracking to them and immediately dismisses many useful ideas

    Java's checked exceptions got the worst possible combination of error tracking. Its optional, so you don't even get to see if a function throws (kind of like the billion dollar null mistake) and its based on classes, which means a lot of irrelevant names leaking throughout the codebase.

    Like with nulls, the main value is being able to claim that a function doesn't ever throw, at all. Apple's Swift got this just right.

    A simpler system of "throws / doesn't throw" and with optionally polymorphic variants / unions to complement it would go much further.

  • by flowerlad on 9/11/20, 10:05 PM

    The biggest mistake of C# is not having checked exceptions. Your carefully written program can crash because someone modified a dependency to throw a new exception. So the only way to make your program resilient against such changes is to catch the base Exception class, which everyone agrees is wrong (because of "swallowed" exceptions). In Java the compiler alerts you if someone modifies a dependency to throw a new exception, which is good.

    See long discussion here: https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...

  • by noisy_boy on 9/11/20, 4:57 PM

    I've found checked exceptions pretty useful. I can pass context-specific details to the exception and encapsulate the message formatting to the exception itself (helps if I'm throwing that in multiple places). They also allow me to decide the http return code based on the exception. E.g. using Spring Boot's controller advise, I can map a group of exceptions to be user errors (say, bad request) and another group to be service errors (say, internal server error) etc and don't have to worry about where the exception is being thrown from - it'll return all the details with correct return code to the user.
  • by CornCobs on 9/12/20, 6:17 AM

    Having used Java somewhat, the biggest pain points I encountered with checked exceptions was their incompatibility with highly generic code (aka streams). What if all library functions taking lambdas (e.g. map, filter) all took throwing functions as parameters (i.e. have an extra generic X argument for the exception type, like R apply(T arg) throws X) and simply rethrew those exceptions, AND have the exceptionless functions (R apply(T arg)) be a subtype of the throwing version so they are compatible? I haven't touched java in a while so I may have forgotten a thing or 2 about its type system
  • by mcculley on 9/12/20, 12:55 PM

    I would prefer there were just a semantic way to find out what exceptions a method might throw. I develop a lot of Java and often end up just rethrowing a checked exception as RuntimeException or AssertionError.

    A long time ago, I developed quite a lot of code in Ada83. Our team found having to use documentation to express what exceptions might be thrown led to many errors. I was pleased when Java came around that this was expressed directly in the function declaration.

    But then it became clear that it lead to a lot of boilerplate.

    I would like the throws keyword to just be an indication of what might be thrown and not require that I catch it.

  • by jayd16 on 9/11/20, 7:39 PM

    I don't mind Java checked exceptions but of the languages in this style I think I prefer C#'s Task type. It combines a promise with a simple IsFaulted boolean and a way to rethrow the caught exception at your discretion (or it will throw if you accidentally access the result getter).

    You can use catch block style with typed exception handling or simple boolean checks depending on the situation and what you prefer.

  • by tomohawk on 9/11/20, 11:40 PM

    Java has checked exceptions, unchecked exceptions, and errors.

    Some checked exceptions, such as InterruptedException, should really be something else. I've very rarely seen this exception handled properly by anyone. Often, a general catch Exception will also catch this, and while the code will work just fine in most circumstances, random threads will not go away when they should.

    It's a mess.

  • by bitwize on 9/11/20, 11:09 PM

    Java's biggest mistake was not including lambdas and generics from the very beginning. Checked exceptions are a feature because they force you to think about how you will handle exceptions: usually one of die, try again, or try something else. Forcing the programmer to make these decisions explicitly is a good thing.
  • by mcguire on 9/11/20, 7:54 PM

    If the exceptions thrown by your code aren't part of the interface, then pretty much nothing is. That means strong typing is actually Java's biggest mistake.

    This isn't to say that checked exceptions are always used well. (What exactly am I supposed to do about an exception from .close()?)

  • by whitenoice on 9/11/20, 6:35 PM

    Most codebases use lombok, you can use @SneakyThrows or @SneakyThrows(SpecificException.class) - https://projectlombok.org/features/SneakyThrows
  • by sreque on 9/11/20, 9:22 PM

    I'm surprised how controversial this is; checked exceptions are a mistake. There is a reason C#, Go Swift, Rust, Scala, etc. don't have them, and it's not because these language authors don't know what they are doing.

    Checked exceptions have all the disadvantages of monads:

    * Checked exceptions don't compose with each other. You have to create manual witnesses of composition (methods throwing multiple exception types, or wrapping exceptions into new exceptions like a monad transformer)

    * Checked exceptions infect the type system, having to be copied everywhere.

    However, they have none of the advantages of monads, and more disadvantages besides. Java the language does not provide any facilities for abstracting over checked exceptions, and they interact terribly with any design involving higher order functions or any other higher-level abstraction.

    It's time for the java community to admit they got this one wrong and move on.

  • by desertlounger on 9/11/20, 9:31 PM

    I write a lot of Java, and like checked exceptions. The problem is rather the opposite, basically all of the concrete exception classes that you might think to use (e.g. IllegalArgumentException) are unchecked!
  • by ncmncm on 9/11/20, 2:58 PM

    Sure and they are a big, big mistake, but biggest? I can name bigger ones. Default virtual is quite the whopper, for example.
  • by franzwong on 9/11/20, 3:36 PM

    I also see people saying they miss checked exception because developers always forget to check the returned error code...
  • by storedbox on 9/11/20, 8:31 PM

    Checked exceptions are hardly Java's biggest mistake.
  • by kanzenryu2 on 9/11/20, 8:31 PM

    It's easy to see that checked exceptions are bad... no other language tried to copy the idea. A worthy experiment, but that's all. What else did not get copied? Classloaders.
  • by tealpod on 9/11/20, 3:28 PM

    My biggest complaint with Java is Generics. I am not against Generics, I don't like the way they were implmented in Java and they copy/pasted the same shit to C#.