This blog entry discusses exceptions and their relationship to other interaction points. I here argue that the throwing and catching of exceptions should be regarded more or less as any other interaction between modules and thus conform to the chosen abstractions of those modules.
There is a constant debate about the virtues of checked exceptions. Checked exceptions mean that the potential thrower, or propagator, of the exception have to declare that potential act of throwing in advance, and that any users of the code have to either catch such exceptions or in turn declare itself as a propagator.
In the naïve use of this model, a ripple effect is seen, where either:
- A chain of client modules all have to declare the potential throwing of that same exception. Pass thru.
- One of the client modules have to handle that exception, potentially thrown from deep down the call chain.
The first alternative seems redundant and tedious. The second one is scary, since the upper-level client module might have little knowledge about the lower-level exception thrown. The typical example of the second scenario is a mapping abstraction throwing a NoSuchKeyException
due to a non-existing key. That exception does not make much sense higher-up the call chain, where the module IncreaseEmployeeSalary
is trying to deal with the fortunate event of an employee getting a raise.
It is quite understandable that developers, faced with these sad scenarios, tend to either silently ignore telling clients about that potential exception, where the language allows for such laziness, or use unchecked exceptions – a class of exceptions being so unpredictable that not even the implementer himself knows that it will be thrown. Unchecked exceptions often deal with resource availability issues, such as OutOfMemoryException
In the face of having to deal with checked exceptions – after all, the serving code could have been implemented by someone else, not as lazy as the client developer – those developers would simply catch those, often lower-level, exceptions and do nothing, but perhaps log it. There is not much knowledge about the exception at that more abstract client level, so what else can one do?Answer: One should treat exceptions with the same respect as any other interaction between modules, i.e., make it part of the API, at the same abstraction level as function calls.
This implies that the IncreaseEmployeeSalary
module should not
throw a NoSuchKeyException
. Whether it informs its surrounding of that potential event – by declaring the throwing of that exception - or not is irrelevant to this discussion. That exception is simply at a completely wrong level of abstraction.
What should happen is that IncreaseEmployeeSalary
is informed of an underlying problem, perhaps directly from that mapping abstraction, if the former happens to be a direct client of the latter. That event should in turn result in another event, possibly throwing an exception at the proper abstraction level
of the former module, e.g., EmployeeNotFoundException
. Note that there should not be an automatic “translation” of the lower-level to the upper-level exception, since the contract of the higher-level module might stipulate some other, non-exceptional, handling of that event.Potential Problems
- Frameworks have no idea what the domain-specific exceptions would be.Yes, that is true, although one could in most cases wrap such framework components in more domain-specific code, which could turn the exceptions to more adequate ones. This is harder with template-based generic constructs, which are even mixed in via inheritance in some cases. This is why most hard-core C++ developers have given up on declaring exception throwing, but simply either wait for that unhandled exception handler, which often terminates the thread, or catch anything at a high level.
- How can you know what exceptions are potentially thrown from deep down?In environments forcing the declaration of such acts, there are ways. The important point here is that the implementer should know, either way, following the principle of treating exceptions as any other API element.Exceptions from deep down should be considered bugs.
- During debugging, at least, one needs to know what INITIALLY triggered this exception.Most language environments allow for nested exceptions, where a higher-level exception can carry the initiator as a sub object.
I am not stating that one should always
convert each exception at each level. It might
be the case that the low-level exception is fully adequate at the higher-level interface.
The main point is that the client should always understand the meaning of the exception thrown from the serving code, talking the “language” appropriate between the two modules. The same modularity aspects as with class methods, in other words.