Wednesday, February 15, 2012

Checked Exceptions Might Have Their Place, But It Isn't In Java

In Java's more than 15 years no language has repeated Java's experiment with checked exceptions, other than some languages designed as Java extensions (e.g. MultiJava and GJ). Certainly no mainstream or even nearly mainstream languages have. Notable languages that don't bother with checked exceptions include C#, which started as very nearly a clone of Java, and Scala, which borrows many Java concepts and then beefs up their static type checking.

Even within the Java community checked exceptions have been at least somewhat deprecated. Spring and Hibernate, for instance, moved strongly away from checked exceptions. Bruce Eckel, author of Thinking in Java, considers them a mistake and Joshua Block, author of Effective Java, cautions against overuse.

Now, I've seen lots of arguments that you should "use checked exceptions when the caller must somehow recover." But that argument assumes that Java is basically a procedural language. It's not. It's an object oriented language where reusable abstractions are common. Just a couple of examples from the standard Java library reveals exactly what's wrong with Java's checked exceptions.

The java.util.Iterator methods, for instance, aren't declared as throwing any checked exceptions. If you create your own Iterator that calls a throwing method then the implementation must do a try {...} catch (CheckedException e) {throw new UncheckedException(e)}. Or worse, you swallow the exception. Either way it's boilerplate.

On the flip side, to avoid that problem the call method on the java.lang.Callable interface declares that it might throw "java.lang.Exception". But Exception tells you nothing about what might fail making it exactly equivalent to the unchecked RuntimeException except that you must "handle" it by either redeclaring it in the throws clause (which just pushes the problem upstream) or doing the try/catch/rethrow-unchecked dance. Again more boilerplate. Or, again, more opportunity to swallow.

You might think generics give a way out of the quagmire, but Java has a fatal flaw here. The throws clause is the only point in the entire Java language that allows union types. You can tack "throws A,B,C" onto a method signature meaning it might throw A or B or C, but outside of the throws clause you cannot say "type A or B or C" in Java. So if you have "interface MyInterface<T extends Exception> {void mightThrow() throws T...}" then T must be bound to a single exception type for any given instantiation of MyInterface. And, as special bonus, with that structure you can't say some particular implementation doesn't throw at all. Which means that in practice it's little better than the java.lang.Callable "throws Exception" solution.

The team working on lambdas for Java has found checked exceptions a major stumbling block essentially for the reasons outlined here. A significant amount of work is going into easing that pain

In short, as it stands the design of the Java language requires you to either avoid reusable abstractions or wrap useless checked exceptions in boilerplate. And if I want to avoid reusable abstractions then I know where to find Pascal. Could checked exceptions be made workable? Perhaps with some careful language design. But Java isn't that design.

11 comments:

shajra said...

I normally think of the selection of checked versus unchecked exceptions as falling across a 2x2 matrix.  On one axis, we have the question:  "Did the client abide by the contract of the interface?  Yes or no."  On the other axis:  "Can the client possibly handle this exception?"  There's only one quadrant where a checked exception even begins to make sense -- when the client has respected the contract, but and error has occurred that can be handled.  The thing is. . . this doesn't happen to often.  Most of the time, we just have crappy contracts that don't make the rules of engagement clear, which leads to a culture of needless defensive programming -- including inappropriate checked exceptions.

After seeing Validation/Error/Either monads, I'm kind of unimpressed by Java's "experiment."  Type safe error handling could have been handled exclusively in the type system, which appears to be Scala's solution.  I'd love to see Java 8 or 9 go in this direction.

In the meantime, it seems we're just suffering the pains of backwards compatibility to accommodate an unfortunate vestige of history.

Leonel Gayard said...

Also, Java's checked exceptions are famous for being enforced by the compiler only, as the JVM ignores such concept and treats all exceptions as runtime. I find this dychotomy so funny, it's as if the VM engineers disagreed with the language engineers but would only shale their heads and avoid any arguments; also, it's like they predicted the presence of other languages on the JVM.

In any case, lovers of alternative VM languages have to thank them. Can we imagine what these languages would look like if the VM enforced checked-ness if exceptions as well as the language ?

(Of course, I'm only speculating about the VM engineers vs language engineers part. I don't know who was in each team. The reasons for this particular difference between language regarding checked-ness of exceptions would make an interesting story.)




(Of course, I'm only speculating

PickedUsername said...

It's a fundamental tension between covariance/contravariance AFAICT. The set of possible exceptions that can *actually* be thrown often tends to *increase* the deeper you go in an inheritance hierarchy while the set of declared exceptions is only allowed to *decrease*. Without whole-program compilation (and possibly serious type system magic) it seems essentially unresolvable (to me).

Btw, I find one of the absolute worst aspects of the

  catch-checked-and-rethrow-inside-a-RuntimeException

idiom is that calling code cannot now even just "catch (E e) { ...}" (say, UnsupportedEncodingException), it has to *somehow* figure out that a RuntimeException contains an UnsupportedEncodingException and handle it. This varies on a case-by-case basis.

So normal "try { ... } catch (...)" syntax no longer works, essentially.

Horrible.

Joel Hockey said...

I think you mean java.util.concurrent.Callable, not java.lang.Callable.

Jesper Nordenberg said...

Checked exceptions are a poor substitute for the Validation concept.

Anton said...

Or, alternatively, checked exceptions can be 'chucked', as one of your article suggests.

Stephen Colebourne said...

FWIW, Oracle rejected the lone throws proposal I put forward for fixing checked exceptions. A mistake IMO.

John Hinnegan said...

Compiler only, I was unaware ... shouldn't we be able to just add a compiler option to stop checking them and be done with it? (or at least, optionally?)

ikk said...

I like to think of checked exceptions as additional return values. They are even stricter, because you can easily ignore return values in Java.


So, for most errors (something unexpected happened deep in the call chain, some method is supposed to be overriden, etc.) checked exceptions do not work well. But if you think of a small procedural piece of code, maybe a checked exception could be useful. Even tho most people say you shouldn't use exceptions for flow control...

Bruno Kim Medeiros Cesar said...

They may indeed be an enforceable return value verification, but shouldn't be used as control because they're slooow to build: Exception construction must fill the whole stacktrace to eventually print it.

A better in-between approach, if you want to ensure return checking would be to create a method annotation like @WarnUnusedResult, which would display a warning if the result was unused - GCC uses this in several input functions, like scanf and fgets.

This warning could be suppressed further up, or disabled entirely with a compilation flag.

Fetrovsky said...

What's the problem with saying that a method throws A, B, Z, Q and T if they're being thrown by dependencies and it doesn't care about them? You don't really need to catch and rethrow, right? You only catch the ones you do care about.