Exception handling is one of the very basic foundations of almost any software system; nevertheless, the relevant approaches vary essentially and conceptually among different languages, different technologies and – many times – different developers. In my opinion, exception throwing has defined a perceptual paradigm change that is even greater than moving from procedural programming to OOP or from strictly typed languages to loosely typed ones. I have met quite a lot of great developers that could adjust themselves to many changes, but were unable to forgo the habit of checking each function’s return value to make sure it has successfully finished. Even worse than that – I know many developers that do use exceptions mechanism, but in such a way that it would have been better to just check the return value instead…
This topic is a very complicated one, and many times there is no single-right-answer regarding how to use it. In this post, I try to provide a few “Do” and “Don’t” rules that will help you get the best out of the exception mechanism.
When in Rome, Do as the Romans Do
Different languages define different approaches to exceptions. In Java, for example, the meaning of “exception” is quite literal: If an exception is thrown, it means that something went wrong. In an utopian system with no problems (like broken network, wrong values, full disks…) – the whole code should have flown using control statements only. In other languages, like Python – things go exactly the opposite way. “It is easier to ask for forgiveness than to ask for permission”, so we will always run forward, until we get stuck in a wall… Instead of checking whether we are at the end of the list in order to decide whether there is a nextElement() or not – we will just read it, and if an exception is thrown – well, it means we got to the end.
Both approaches are good, as long as they are implemented in the right context. In Java – do as the Javas do. In Python – do it the Pythonic way. Personal standpoint about the “right” thing is important, but if implemented in the wrong domain, it will result in a code that is completely unreadable through the eyes of all other developers with which the code is (or will be) shared, and it will act completely different than all the other pieces of code it connects to. Not less important, is to pay attention to the very specific context in which you are programming. If you started working this morning in a company where the code is written in Python, but its founders are Java people that decided to treat exception the way they use to – don’t try to teach them how Guido would have done it. Keep working under the local convention – or no one will ever understand your code, not to mention integrating it.
Keep the Logic Clear
More than once have I encountered programmers that got so enthusiastic about the concept of exceptions, they tried to catch them everywhere. A ten-lines function might inflate to 50 lines in seconds, when wrapping each call with try-catch blocks. Apparently, there might be cases where this is the right thing to do – but those are very rare. Usually, the right thing will be to wrap the whole ten-lines-block with a single try-catch, and handle all the relevant exception in a single point (if they should be handled within that function at all – but on this, later in that post). Of course, it is important – on the other hand – to make sure that the context of exceptions handling is clear as part of the code’s logic. There is no point in having the try-catch block in a far location, that does not let the reader any understanding for their reason in the first place.
Make Sure that Exceptions Improves Stability – and not the Opposite
The goal of exception handling is to make sure that the system behaves well even during unpredicted situations, that do not allow it work the way it should. Misuse of exception throwing might lead to the exact opposite. Code that is written in a way where exceptions just stops whatever the program is doing and “jumps” up the call-stack might leave the system with open ports, orphan or zombie processes and threads, useless handles, leaked memory, broken data structures and, in fact, in a much worse condition than just failing to execute some function call. Each language and system has its own tools to deal with those situations – whether as an inherent part of the language exceptions syntax (e.g. a “finally” statement), using smart-objects (RAII) or any other way. Without a clear understanding of those mechanisms – there is no point in exception handling.
In his book “Effective C++”, Scott Meyers defines two fundamental rules that code must obey in the case of exception:
- An exception-safe code will never allow resources leakage;
- An exception-safe code will never leave a “broken” data structure.
In addition, he defines three levels of “Exception Safety” (more precisely: he describes those guarantees, which were defined by David Abrahams) that allow writing robust systems. In order for the system to be immune to exception throwing, each and any call in it must assure at least one of the following guarantees:
The Basic Guarantee
A function that provides the “Basic Guarantee”, promises that in a case it throws an exception – the system will be found in some legal condition. There is no guarantee about the system state itself, and values might change (for example, resetting to some default values or even loss of data); but the system, as a whole, will be in some legal state. For example, suppose that our system deals with triangles. The length of any two sides in a triangle must sum up to more than the length of the third side, or that is not an Euclidian triangle. Now, if we call a function that changes the lengths of a triangle’s sides and it throws, it must make sure that it leaves the triangle with legal values in order to provide that “Basic Guarantee”. It is possible that the triangle was not changed at all. It is possible that only one or two of its sides were changed. It is even possible that it was completely changed to some default “all ones equilateral” triangle. But it is assured to represent a legal triangle.
That might sound like a very weak guarantee – think of a UX designer that has lost all the changes they made… That might not be very pleasant; but from a programmer point of view, it is still very important: It makes sure that there will be no undefined (or unexpected) results to any further action – e.g., calculating the triangle’s area.
A “legal state”, of course, does not have to be a geometry theorem. What “legal” means is defined by the system designer. The important thing is, that those definitions are known across the system, so that other functions expect them to exist. For example, a system might define that the color of all the items in a given menu must be the same. In such a case, leaving some items in their original Red and changing a few to Blue breaks the Basic Guarantee. If failure to change all of them from Red to Blue results in changing them all to White – well, it might be ugly, but it satisfies the guarantee (think, for example, on another function, that acts on all the items, but only asks the first one for its color, knowing that this must be the same for all the other items as well).
The Strong Guarantee
Providing the Basic Guarantee we have just described might provide a stable and robust system – its popularity, however, is questionable… Whenever it is possible, it is better to provide the “Strong Guarantee”: A function that runs under the Strong Guarantee is responsible to leave the system (after it finishes, or throws) in one of two possible state only: Either it fully succeeds, and does everything it had to (for example: Changing all the triangle’s sides as required; changing all the items from Red to Blue) – or it does nothing at all, leaving the system in the very same condition it was before calling the function (e.g., changing nothing in the triangle; changing no item’s color). In fact, a function that provides the Strong Guarantee acts as a transaction.
Of course, writing functions that provide the Strong Guarantee generates a much better code. However, it is not always possible, and in some cases, it might not worth it – for example, if it requires much more development time or creates much more complicated code.
The No-Throw Guarantee
This is the highest level of guarantee a function might provide: Making sure that the required task is always being done, with no exception ever been thrown.
In a Case of Swallowing, Proceed Immediately to a Hospital!
Exception swallowing is a common practice in many places. The meaning of “swallowing” is that one calls a function that might throw, and makes sure that any exception is not being forwarded from that point up, despite not being actually handled. In the most extreme case, it is about swallowing any type of exception and completely ignoring it. In other cases, it is about swallowing only certain types of exceptions, or some minimal handling, like no more than logging or so.
There are cases where exception swallowing makes sense. This is mainly true in the “Pythonic” approach. For example, if we are to go over a bunch of not-very-updated list of some DB tables, count the number of records in each of them and ignore those from the list that are no more existing – one might decide to run the action for all the tables in the list, and just swallow any exception being raised by trying to open a no-longer-there table. And yet, a simple exception swallowing in such a case is still dangerous. What if we have a connection issue? Or our SQL is illegal? Or we have no access right due to some configuration issue? If we use some generic “catch”, we will never know that.
Therefore, even in those cases where it makes sense not to handle some exceptions, the right thing will be to define the exact specific exception type that we would like to ignore, and make sure that any other type is being handled.
Many times, a catch-all swallow is only a leftover from the development or debugging stages, where someone wanted to focus another issues without being interrupted by exceptions being raised due to other reasons, and just forgot to remove it. Therefore, as a rule of thumb – during any code review or commit, it is very important to identify cases of exceptions being ignored and verify that this is done deliberately. If it is – make sure there is a relevant comment, even if this seems to be a “self explained code”.
What Exactly Should We Throw?
In one of the coming posts, I will discuss the issue of exception types. Stay tuned!