Exceptional programming
05 July 2014Writing about exceptions is something I have been meaning to do for some time, and much of the text in this article was drafted some considerable time ago, and almost all the ideas are ones that I have had for some considerable time. My overall thought is that exceptions are good in theory, but their practical use is almost universally bad.
Checked vs. unchecked exceptions
Checked exceptions is something that is pretty much only found in Java, and they are exceptions that have to be explicitly handled by the programmer. In Java checked exceptions are things like File-Not-Found that really should be handled as soon as possible, and Java requires them to either be explicitly caught, or added to the list of exceptions the function may throw. Java also has unchecked exceptions, which are typically the result of programming errors such as array overruns. Of course there is not a clear boundary between what should be checked and unchecked, but that is more a policy issue than a language design issue.
Problems start because Java tends to use a separate exception for each possible error, which reveals Java's one big mistake: Unhandled checked exceptions are regarded as compile errors rather than warnings. Lots of people simply got into the habits such as putting
C# is plainly a clone of Java, and its decision to do away with checked exceptions is that they tended to go against the grain of practice. In my first job using C#, I frequently got my fingers burned because a load of exceptions I should have added catches for were often buried in the depths of the documentation, and to me it was obvious that C# blamed the concept of checked exceptions rather than understanding their misuse. Having to take account that there may be one of countless exceptions going flying at any moment adds an extra layer of complexity to programming, which goes against the usual trends of avoiding stuff that trips up programmers.
This view is clearly affected by the types of errors I have historically dealt with in the programs I have written, which in turn is the profile of errors inherent in network programming. This is a fairly large number of errors that have to be expected from normal operation, a small number of errors that are probably not worth even trying to recover from, and pretty much nothing in between. If something goes wrong, all that can really be done is to dump the packet and/or connection, and get on with dealing with everything else that is incoming or outgoing.
throws exception
everywhere, in effect turning all checked exceptions into unchecked exceptions. At least with warnings, someone could go around at later date and add proper exception handling.
Most of the arguments I have seen against checked exceptions are in reference to large-scale systems that use Object Factory approaches, where the programmer can give arbitrary classes as parameters. I think such systems where disparate APIs are wrapped into each other is a recipe for a complete mess anyway, so it is hardly surprising that explicitly handling all possible exceptions becomes a logistical nightmare.
Just too many
Java's checked exceptions were annoying in large part because it used a whole load of different exception types, rather than one or two generic exceptions that contained codes further describing the error. I can't remember if there was a hierarchy of exception types, but there were so many that this would not have helped much in any case. Having to explicitly deal with one or two checked exceptions is just about tolerable, but dealing with dozens very rapidly gets annoying. Having a specific exception for each error type carried over to C#, where the problems became even worse.
Exceptions should be exceptional
In my view, exceptions should be used for errors which are not expected from normal operation, and such an error is of a nature that it cannot realistically be dealt with as soon as it it detected. Exceptions handling by its nature involves falling back, and the way it unwinds the function call stack is not lightweight. As a rule-of-thumb, I would be most inclined to throw exceptions in situations where it would normally be justified to use assert()
or exit()
, such as a memory allocation failure. For errors that should be expected in normal operation such as internet address resolution failures, reporting them via exceptions actually makes dealing with them there-and-then harder.
Exceptions simply wrong?
The most concise critique of exceptions I've seen is that by Joel on Software, and in large part I agree with it. Exceptions are conceptually nice things, but they bring a whole load of pit-falls with them, and more often than not are used as a poor substitute for decent error signalling in return values. When I was getting back into C# while still in New Zealand, I came across the concept of exception neutral code, which to me highlighted the hoops that have to be jumped through to get around such pit-falls. The declared solution of exception neural code was to always do work that might throw an exception on a copy of the data objects, and then commit it by replacing the original object references. This is something that is predicated on the costs of working on a copy being low, and in practice this means using a language that has garbage collection. At the time my most recent job was in network video programming, where doing such a thing would not really be within the bounds of sanity. Clearly the type of programming for people who have no concept of memory management.
Exceptions in Python
While on the whole I think Python's overall use of exceptions is about right, I dislike the Pythonic programming principle of EAFP, which encourages prolific use of try-catch
block rather than thinking about where an error might occur. This relegates considering failure cases to afterthought, and in the extreme completely divorce it from integrated programming. I have seen code that amounted to the following:
def doProcess(self, value) if value in valid_range: self.doSomethingElse() return true else: raise BadValueError()
The problem here is that error conditions are something to be thrown up in the air, on the assumption that someone else will catch them and clean up the mess. In this case the usage is particularly bad, as there is an alternative way of reporting the details of errors:
def doProcess(self, value) if value in valid_range: self.doSomethingElse() return None else: return [BadValueError()]
I have found that when demands for higher code coverage kick in, all those localised exception catch blocks that take the place of checking return values have a habit of disappearing in favour of a few top-level catch statements, assuming there was any of the former in the first place.