If you haven’t already, go on over and read an interview that Anders Hejlsberg gave to Bruce Eckel and Bill Venners: The Trouble with Checked Exceptions. Think about it for awhile. Wait, no, just a bit longer. Ok. Here we go. In fact, I don’t even know where to begin. I think I’ll start with my favorite, logical fallacies.
First, there is a common logical fallacy that is used over and over again (both in this article and in general about the topic of checked exceptions), and if you don’t pay too close attention, you’ll miss it entirely. Here’s one that you see every once in awhile (apologies for any straw man-isms, but this is just an example, not Anders’ point in particular):
Checked exceptions are a major failure in Java. Have you ever tried to use JDBC classes? Every single method declares that it throws a SQLException. With checked exceptions, you run into this issue of exception bloom, where lower exceptions cause problems all the way up, and you end up writing “throws SomeGeneralException” on every friggin method.
Let’s break this down into its primary components:
- JDBC classes handle exceptions badly (perhaps not the worst way possible, but not well)
- This handling badly is done in Java, and the exceptions are checked exceptions
- Checked exceptions are bad
That is about functionally equivalent to:
- Fiats are ugly.
- A fiat is a car.
- Cars are ugly.
In the more general, the argument is this.
- Implementing checked exceptions like this is bad / ugly / unwanted / unwieldy / whatever
- The way in which this is implemented is an inherent property of checked exceptions
- Checked exceptions are bad / ugly / etc.
The problem is that nobody really backs up the second step.
Alright, let’s move back to the article. Anders has two primary points for why checked exceptions are bad. First, making exceptions a part of a method’s signature has versioning issues:
Anders Hejlsberg: Let’s start with versioning, because the issues are pretty easy to see there. Let’s say I create a method
foothat declares it throws exceptionsA,B, andC. In version two offoo, I want to add a bunch of features, and nowfoomight throw exceptionD. It is a breaking change for me to addDto the throws clause of that method, because existing caller of that method will almost certainly not handle that exception.Adding a new exception to a throws clause in a new version breaks client code. It’s like adding a method to an interface. After you publish an interface, it is for all practical purposes immutable, because any implementation of it might have the methods that you want to add in the next version. So you’ve got to create a new interface instead. Similarly with exceptions, you would either have to create a whole new method called
foo2that throws more exceptions, or you would have to catch exceptionDin the newfoo, and transform theDinto anA,B, orC.
Anders makes the assumption that if a foo now throws an exception D, that it is bad to have a breaking change.
Let’s step back a minute and think about what it means to have a breaking change. In most cases, you need to weigh the (possibly) far-reaching outcome of changing every single consumer of what you are writing with the benefits you gain from the change. In the case of large frameworks, often the first outweighs the latter. This kind of inertia is what keeps heavily consumed things in a mostly static state. On the other hand, with small projects, the cost of the first is usually minimal (e.g. clicking “rename this” on your IDE or some type of find/replace on your editor), and any benefit will make the change worth it.
There is another issue we need to deal with, and that is our conceptual understanding of what a method’s “interface” is. Unfortunately, some arguments in this area suffer from a bit of circulus in demonstrando, meaning that the rationale for why exceptions aren’t a part of the concept of interface is because, well, they aren’t part of the concept of interface in whatever given language (and the reason they aren’t in whichever language is because they aren’t in the concept of an interface). The truth is, in the world of checked exceptions, the same argument applies, although in the opposite way. So we need to try and take a step back, and think about what exceptions actually are.
Exceptions say “wha?”
So, what I really mean is what exceptions are conceptually, not necessarily how they are implemented in any given language. This takes a bit of background work, so here are some definitions in my little world:
- A function is a contract that says “If you give me these things, I’ll give you those things. Promise.” In most languages (i.e. those that allow side-effects), this also includes “Oh, and I might change the state of the world.”
- A method (in most C-style OOP languages) is a contract that says “If you give me these things, I’ll do some stuff, possibly change some state, and then maybe give you something back.”
- An exception (on a method or function) is a message that says “Hey, I can no longer fulfill this contract, but I need to inform you in a meaningful way. Sorry, bub.”
Great. So now, within the concept of exception, we can split these into two different things:
- “Holy crap, something incredibly horrible just went wrong and there ain’t nothin’ I can do ’bout it. You either. But I just wanted you to know.”
- “Hey, something went wrong, but it isn’t that bad. Maybe you can fix some stuff and try again?”
Now, as somebody who wants to learn about one of these here “contract” thingies (i.e. functions/methods), I want to know as much about it as I can. More specifically, I want to know what kind of things to give to the contract, what to expect in return, and what the function does (yeah, that’s a biggie). In a language with exceptions, it would also be nice to know about that second class of exceptions.
In a sense, the second class is really just a different type of return value, like “true”, “false”, and “whoops!“. The problem is that, in many languages, it is difficult to say “I promise to return type A, unless, of course, this happens, and then I’m going to return a type Foo“. So the languages decided to work around that by creating this thing called exceptions, which is effectively just that return type Foo, with some other syntax to make checking for that special return type easier. Also, you get this neat side-effect (in most languages) that if you don’t want to deal with the exception, you just let it fly up to whoever called your contract, and so on.
Back to Anders
So, as the consumer of a contract, I would like to know about that second type of exception. If there is some type of way that the function can inform me something like “Hey, that parameter is no good”, or “Hey, try me again under a different state of the world (i.e. maybe you are in the wrong directory?)”, then I want to know that. I also believe that that portion of the contract is just as important as, say, the number and types of parameters.
One of the things about Java that makes me happy is the manner in which exceptions are handled (no pun intended). You have specific types for the two classes of exceptions: Error or RuntimeException (”holy crap!”) and everything else that is Throwable (”hey, fix this!”). The first type, Error or RuntimeException, you don’t need to declare as a part of your exception contract. This is because that type of problem is not generally recoverable, and there is no reason to force whoever is using your contract to deal with it. The other type, the “fix this!”, says “Hey, you should either fix this or fail yourself, hopefully in some meaningful way.”
Note that the correct way to design exceptions and handle exceptions is not to just keep throwing that same exception. Anders’ second major point is this:
Anders Hejlsberg: The scalability issue is somewhat related to the versionability issue. In the small, checked exceptions are very enticing. With a little example, you can show that you’ve actually checked that you caught the
FileNotFoundException, and isn’t that great? Well, that’s fine when you’re just calling one API. The trouble begins when you start building big systems where you’re talking to four or five different subsystems. Each subsystem throws four to ten exceptions. Now, each time you walk up the ladder of aggregation, you have this exponential hierarchy below you of exceptions you have to deal with. You end up having to declare 40 exceptions that you might throw. And once you aggregate that with another subsystem you’ve got 80 exceptions in your throws clause. It just balloons out of control.
If anything, Anders is just helping prove that point that propagating every exception underneath you is a bad idea. (His conclusion, that this means that “checked exceptions are bad”, is a giant stretch of logic). If the exception doesn’t make sense outside of the scope of calling it, you need to encapsulate it inside of something else. This is the same way encapsulation and aggregation work in the programming world anyways; I wouldn’t expect to pass to a function, Foo, all of the parameters of functions that it calls. For a more humorous perspective on this, read Why I Love Anders Hejlsberg. Again, a FileNotFoundException might not make sense to the caller of FooCollection->AddFoo(…), but an exception that looks like CollectionStateInvalidException may make sense, especially if it is well known that the state of the collection may become unstable. It’s all about encapsulation.
n the case of C#, I can’t tell my caller what types of exceptions I would throw, and my caller can’t determine (without some heavy lifting in the form of IL analysis) that by himself. This really isn’t a problem in my own work, as I declare my exceptions in doc comments. Unfortunately, most of the APIs I consume have large holes in their documentation, and specifying exceptions seems to be one of the first things to go. Whoops! I’m left to attempt to discover what exceptions might be thrown, and hope that that list doesn’t ever change in the future, because I have no way of versioning in relationship to exceptions.
See, when I first read Anders’ first point, I figured that it mostly made sense, since breaking changes for large systems can be a very bad thing. However, when you look at the contrary, where someone can change the “hey, you need to fix this!” list of exceptions without ever telling your caller, you may have an even bigger problem. This exact thing happened to me, where massive changes made between different versions of a Beta product ended up changing the context in which something happens. This manifested itself to the caller by the type of exceptions that it could throw. The problem? Most of the public API was uncommented, and the only reason I found the new exceptions was because of a bug elsewhere in the code.
What not to do
Bruce Eckel, in Thinking in Java, proposes that checked exceptions are no good, and proposes that all exceptions should be wrapped in RuntimeExceptions. He explains this more in depth in this article, from his website. He solution is good in that there is no loss of information, but effectively falls in the same class as C#’s answer, although slightly worse (since people consuming any type of interface that follows that methodology will find something quite contrary to what the language is telling them to expect). In short, it is the wrong answer.
This gets into another issue, which is breaking that which the language enforces. When I write Java, I expect exceptions of the things I consume to be declared. In the same token, when I write C#, I resign myself to specifying exceptions in doc comments, and hope that the things I consume do the same.
So, as much as I find the lack of checked exceptions in C# to be a pain, I would not try to circumvent the paradigms that the language sets forth.
The thing to do, then, is just to think about it. I mean really, think about your conceptions about these important things, like exceptions, functions, contracts, whatever. The end result, even if you think checked exceptions are retarded, is that you will be aware of the problems either way, and it will help you write and design better code (hopefully).
Also, if you are writing C# code, use doc comments and list your exceptions. It will make everyone else’s life easier.