Tuesday, June 19, 2007

Remove checked exceptions?

Recently, Neal Gafter mused about whether we should consider removing checked exceptions from Java.  The motivation from this was not what you might expect, but rather an observation that checked exceptions interacts heavily with a lot of other language features, and that evolving the language might be easier if we were willing to consider removing some features.  (Neal knows this won't ever happen, he's just trying to get us thinking about Life After Java.)  Not surprisingly, it generated a storm of comments, ranging from "hell yeah!" to "hell no!".

This isn't a new topic; it comes around every few years.  A few years back I wrote about the debate surrounding checked exceptions, and the debate continues to rage.  My problem is that I think most of the vocal opponents of checked exceptions are objecting for the wrong reasons (back then, I wrote: "My opinion is that, while properly using exceptions certainly has its challenges and that bad examples of exception usage abound, most of the people who agree [ that checked exceptions are a bad idea ] are doing so for the wrong reason, in the same way that a politician who ran on a platform of universal subsidized access to chocolate would get a lot of votes from 10-year-olds").

Reading through the against-checked-exceptions commenters on Neal's blog, we can divide them into three primary groups:

  1. "I don't like checked exceptions because they're too much work." 

  2. "Checked exceptions were a nice idea in theory, but using them correctly makes your code really ugly, and I'm left with a choice of ugly code or wrong code, and that seems a bad choice." 

  3. "Checked exceptions are a good idea, but the world isn't ready for them."  (Frequent refrain from this group: "Man, have you looked at some of the code out there?")


To the people in camp (1), I say: engineering is hard -- get over it.  Error handling is one of the hardest things to get right, and one of the easiest things to be lazy about.  If you're writing code that's supposed to work more than "most of the time", you're supposed to be spending time thinking about error handling.  And, pay for your own damn chocolate. 

To the people in camp (2), I have more sympathy.  Exceptions do make your code ugly, and proper exception handling can make your code really ugly.  This is a shame, because exceptions were intended to reduce the amount of error-handling code that developers have to write.  (Ever try to properly close a JDBC Connection, Statement, and ResultSet?  It requires three finally blocks.  Ugly if you do it right.  But, almost no one ever does it right.  (The real culprit here is that close() throws an exception -- what are you supposed to do with that exception?  But that's fish under the bridge.)) 

But perhaps there's a way to not throw the baby out with the bathwater, by providing better exception handling mechanisms that are less ugly.  Dependency injection frameworks did a lot of that for us already, for a large class of applications -- and the code got a lot prettier, easier to write, and easier to read.  AFAICS, the two biggest removable uglinesses of exceptions are repeated identical catch clauses and exception chaining. 

The repeated catch clause problem is when you call a method that might throw exceptions A, B, C, and D, which do not have a common parent other than Exception, but you handle them all the same way.  (Reflection is a major offender here.) 

public void addInstance(String className) {
    try {
        Class clazz = Class.forName(className);
        objectSet.add(clazz.newInstance());
    }
   catch (IllegalAccessException e) {
        logger.log("Exception in addInstance", e);
    }
    catch (InstantiationException e) {
        logger.log("Exception in addInstance", e);
    }
    catch (ClassNotFoundException e) {
        logger.log("Exception in addInstance", e);
    }
}

You'd like to fold the catch clauses together, because duplicated code is bad.  Some people simply catch Exception, but this has a different meaning -- because RuntimeException extends Exception, you're also sweeping up unchecked exceptions accidentally.  You can explicitly catch and rethrow RuntimeException before catching Exception -- but its easy to forget to do that.

public void addInstance(String className) {
    try {
        Class clazz = Class.forName(className);
        objectSet.add(clazz.newInstance());
    }
    catch (RuntimeException e) {
        throw e;
    }
    catch (Exception e) {
        logger.log("Exception in newInstance", e);
    }
}

My proposal for this problem is to allow disjunctive type bounds on catch clauses:

public void addInstance(String className) {
    try {
        Class clazz = Class.forName(className);
        objectSet.add(clazz.newInstance());
    }
   catch (IllegalAccessException | InstantiationException | ClassNotFoundException  e) {
        logger.log("Exception in addInstance", e);
    }
}

My compiler friends tell me that this isn't too hard. 

The other big ugliness with exceptions is wrapping and rethrowing:

public void findFoo(String className) throws NoSuchFooException {
    try {
        lookupFooInDatabase(name);
    }
   catch (SQLException e) {
        throw new NoSuchFooException("Cannot find foo " + name, e);
    }
}

Now, the wrap-and-rethrow technique is very effective -- it allows methods to throw exceptions that are at an abstraction level commensurate with what the method is supposed to do, not how it is implemented, and it allows you to reimplement without destabilizing method signatures.  But it adds a lot of bulk to the code.  Since this is such a common pattern, couldn't it be solved with some sort of declarative "rethrows" clause:

public void findFoo(String className) throws NoSuchFooException
rethrows SQLException as NoSuchFooException {
    lookupFooInDatabase(name);
}

The rethrows clause is part of the implementation, not the signature, so maybe it goes somewhere else, but the idea is clear: if someone tries to throw an X out of this, wrap it with a Y and rethrow it. 

An alternate approach to this would be possible with closures and reified generics; it would be possible to write a pseudo-control construct that said "execute this closure but if it throws X, wrap it with a Y and rethrow it."  Unfortunately, with the current state of generics, we can't write such a generic method, we'd have to write a separate one for each exception type we want to wrap. 

These approaches focus on the symptom -- because the arguments in group (2) are about symptoms.  If we could alleviate the symptoms, people might grumble less.

The people in camp (3) are saying something slightly different.  I don't really have an answer for them, because what they seem to be saying is that no matter what mechanism you give people for dealing with failure, they won't follow it.  Checked exceptions were a reaction, in part, to the fact that it was too easy to ignore an error return code in C, so the language made it harder to ignore.  This works on a lot of programmers who are slightly lazy but know that ignoring exceptions is unacceptable, but apparently is worse than nothing for some parts of the population.  (We'd like to take away their coding rights, but we can't.) 

 Checked exceptions are a pain, and in some frameworks (like EJB before dependency injection), can be really painful.  Once the ratio of "real code" to "error handling code" rises above some threshold, readability suffers greatly, and readability is a fundamental value in the Java language design.  Even if the IDE generates the boilerplate for you, you still have to look at it, and there's a lot of noise. 

On the other hand, my experiences using third party C++ libraries was even more painful than anything Java exceptions have ever subjected me to.  Virtually no packages ever documented what might be thrown, so you end up playing "whack a mole" when exceptions did pop up -- and usually at your customers's site.  If people are not forced to document what errors their code throws, they won't -- especially the people that the people in camp (3) are afraid of.  As long as those folks are allowed to code, the value we get from checked exceptions forcing developers to document failure modes overwhelms the annoyances.

But, as I said above, I think many of the annoyances can be removed by adding a small number of exception streamlining constructs.  This doesn't help Neal with simplifying closures, but it does help us get our job done with a little less pain. 

Finally, a meta-note -- its really easy to misinterpret the volume of support for "removing checked exceptions" as any sort of gauge for community consensus.  We're intimately familiar with the pain that checked exceptions cause; we're substantially less familiar with the pain that they free us from.  (Obviously, neither approach is perfect, otherwise there'd be no debate.)

15 comments:

  1. Occasionally, a framework can help -- that is what the Spring guys did with the JDBCTemplate stuff. To address the SQLException problems you describe, they wrapped (actually "translated" according to the Spring docs) it into a more meaningful DataAccessException with better diagnostics about what actually went wrong in the DB (taking into account vendor specific error codes). And it is unchecked because they reason that there is not much you can do to recover from most DB problems. I think this feature is one of the most useful aspect of Spring. The Spring JDBC support shrunk my JDBC code enormously getting rid of all those catch finally blocks, and cleanly releasing the DB resources.

    ReplyDelete
  2. Thoughts on (Un)checked Exceptions...

    Brian Goetz shares his thoughts on the (recurring) idea of removing checked exceptions from the Java language in a future version, because they tend to make code too ugly to read or are just considered a "failed experiment".
    I personally count mys...

    ReplyDelete
  3. Bad API design can put a big burden of exception management on client code. And JDBC is one of the biggest offenders. Not only should the various close() methods not throw SQLE, but JDBC is really bad about using the exception class hierarchy to separate errors of the "I can't find the database" variety from "Inserting that row would violate an integrity constraint." As a result, unless you want to parse the error message (blech) or use DB-specific sqlState codes, you can't do intelligent error recovery. The Spring approach recognizes that and promotes all exceptions to "silently fatal", but a better approach would let the program tell the recoverable ones from the unrecoverables...

    ReplyDelete
  4. 3) => You are free to contribute to the Spring Framework! :)

    When it comes to your third party C++ library example, that's a no-argument. I can write an equally bad library in Java that doesn't document the RuntimeExceptions that it throws, so checked exceptions are of no help. It would be an argument if Java allowed checked exceptions only, then every layer of an application would always be aware of all the exceptions that can surface, whether they are wrapped or not. In this scenario all methods should probably throw NPE by default, so you'd always have to handle at least NPE whenever you call a method. But that'd be totally unusable, we can all agree on that.

    Since Java allows wrapping of checked exceptions into RuntimeExceptions, their existence does not guarantee quality any more than return codes did in C, and thus makes them futile. And the fact that Sun made it very important in their Java Tutorial for programmers to mostly use checked exceptions doesn't help either. It leads to a lot of situations where checked exceptions are only a nuisance. JDBC is just one example of a library flipping you off and saying "oh, all these methods can throw this horrible exception, and you have to handle it, but you'll never find out the real reason for it". How do you then handle that?

    It all comes down to programmer responsibility: If you document what (Runtime) exceptions your method can throw, then nobody can blame you. The caller either handles them, lets them through and documents them, or lets them through and doesn't document them (bad!). But nobody can blame you for an unusable API. People will on the other hand get annoyed if you throw them a checked exception they can do nothing about, or they know will never actually be thrown (like for String.getBytes("UTF-8")). APIs like that are not corner cases, they're almost all 1990's/early 2000 APIs.

    Your third party C++ library would have been easily fixed and totally backwards compatible by just adding the proper documentation for exceptions. But how do you fix JDBC?

    PS: Your book (JCiP) rules! :)

    ReplyDelete
  5. Sorry, martti, that's a nice theoretical argument, but it doesn't hold up in practice. I did enough programming in C++ for this to be one of the biggest pain points -- and the corresponding problem in Java happens far, far less.

    You're right, terrible programmers can write terrible code in any language. But one of the obligations of language design is to discourage average programmers from doing things they know they shouldn't, but are too lazy to stop themselves.

    ReplyDelete
  6. My 2C on this. If we could use techniques like annotation to indicate to compiler that the class is capable of handling checked exceptions or not, then it would kind of simplify things a lot. If indicated that the class is capable of handling checked exceptions, the compiler should force the class to handler all the checked exceptions thrown in methods invoked by the class.

    ReplyDelete
  7. You fix JDBC by switching that exception to extend RuntimeException.

    ReplyDelete
  8. One more about "allow disjunctive type bounds on catch clauses". But I always think about it using "," instead the "||" operator. Moreover, is not so hard to do it while keeping bytecode compatibility.

    Kind Regards

    ReplyDelete
  9. I love the multi-catch proposal, but there's one detail that everybody seems to skip over - what is the type of 'e' in the catch block? I can think of several options:

    1. some (the least?) common supertype of all mentioned types
    2. Throwable
    3. An implicitly-defined "duck" type (i.e., it really is just syntactic sugar)

    1. could be confusing (programmer has to work this out and it could change -- not safe)
    2. safe, simple, clear, but possibly not very useful
    3. convenient, simple-enough to understand, but goes against Java's static-typing grain

    An alternative would be to require (or allow, with the default being Throwable) a type to be specified:

    catch (Donkey | Goat | Orangutan as HairyThing e) {
    // ...
    }

    [I first saw this here: http://members.capmac.org/~orb/blog.cgi/tech/java/Better_exception_sy.html]

    ReplyDelete
  10. @michael: There are a couple of options, but Throwable is not one of them -- it is important to be able to rethrow it. This also eliminates the least-upper-bound approach, since that is almost certainly Exception, and you usually can't rethrow that either. So one approach would be to represent it explicitly as a disjunctive type. This would have the effect of behaving as the least-upper-bound (duckwise or not), but still allow rethrowing.

    In practicality, the obvious implementation is for the compiler to expand it into N separate catch blocks for you, one exception each.

    ReplyDelete
  11. The real Mr. FunkMay 27, 2008 at 9:42 PM

    I'm all for checked exceptions. It's really not that hard to catch and abort / rethrow them you whinging bastiches :)

    However, I'm also all for catching multiple (named, unrelated) exceptions with one code block, and even managed to get the Posse discussing some ill-thought-out syntax I'd cooked up. It'd be useful even if e comes into the block as Throwable - you can always cast it or use the old exception handlers.

    ReplyDelete
  12. I think good idea would be more IDE support for unchecked exceptions and people will just stop using checked. If method declare runtime exception then you will never know about it if you dont look at method definition and thats not what we do. IDE/compiler could inform you that you are ignoring runtime exceptions (from throws of that method). Some hint in the idea would be cool.

    ReplyDelete
  13. We need a third type of exception. Checked exceptions cover things that went wrong but you have a reasonable shot at recovering from. You can't open a file because it doesn't exist? Throw an exception and let the code prompt the user to try again. (You should still call exists() first, but you might hit a race condition where the file exists when you check for it but is deleted by the time you try to open it.)

    Traditional unchecked exceptions are things that are seriously wrong and unrecoverable in any meaningful manner. You got an NPE -- wtf are you supposed to be able to do to recover from that? You can't. All you can do is log it and either terminate the application or reset to a 'safe' point. Trying to do anything else, e.g., to 'work around' the exception, is a seriously bad decision. Unfortunately it's a disturbingly common one as well.

    The new type of exception would be unchecked and for things that are seriously wrong and unrecoverable -at the original level-, but which can be meaningfully addressed at a higher level. A dropped database connection, for instance. If you lose the database connection, maybe you can sleep for a few seconds and try again later. You might lose some work if you don't use checkpoints but you don't have to kill the entire application.

    I think Spring has tried to model the third type of exception, but it would be nice if there were some way for the language to support it. Especially if we could somehow easily create exception firewalls, for lack of a better term, that would convert the third type of exception from unchecked to checked. E.g., your business logic bean packages don't need to worry about db connectivity issues and the exceptions should be unchecked, but perhaps the next layer up should see them as checked exceptions coming from all of the business logic bean methods.

    Hmmm, you could probably get close with AOP interceptors. I just did something similar on my current project. IceFaces does not handle runtime exceptions well, so I created an interceptor for our business logic classes that converts all runtime exceptions into faces exceptions that can be handled later.

    ReplyDelete
  14. A nice bit of "syntactic sugar" would be for some new superclass exceptions. For example, define a new java.lang.ReflectionException and then take the existing reflection-related exceptions and make them extend the new ReflectionException instead of just Exception. And then do the same in other areas where there are a number of related exception types. This would be trivial for Sun to do and completely backwards compatible.

    Just look at the number of direct known subclasses of Exception in the JDK. There should be maybe 1/4 or 1/5th of that many classes directly overriding Exception, with the rest overriding the more categorical Exception. The perfect model for this is IOException.

    This would remove much (but not all) of the need to have multiple catch blocks.

    ReplyDelete
  15. @Eddie: Or even interfaces. Error and RuntimeException could implement a new interface UncheckedThrowable, so you could catch all unchecked exceptions at once.

    ReplyDelete