Exceptional Exceptions, Demystified - Part 1
Introduction
This article walks readers through a crash-course on what exceptions are in software engineering, and how to properly use and handle them. This article is mainly focused on beginners. However, experienced software engineers can also benefit from this article too, as it may provide some new insights, or perspectives into exceptions and the proper handling therein.
I will be releasing this in multiple parts. This first part introduces exceptions using examples written in different languages. This is the most basic of the parts that I will be publishing in this series.
Part I: Understanding Exceptions
One may be surprised at the simplicity of what an exception truly is. An exception in software differs little from the common English definition of the word “exception”. In common English, an exception is defined as “An instance or case not conforming to the general rule.” In other words, as applied to software engineering, an exception is an instance or case that should never happen under normal circumstances.
Many programming languages, such as Java, C# and C++, have “exception handling” to simplify handling exceptions. As minimizing complexity is the primary directive of software engineering, exception handling can be a useful and powerful tool for minimizing said complexity, if only implemented correctly.
An introduction to the exception handling constructs that are provided in many of the programming languages is provided below:
The following observations are made of some of the keywords in the example:
- throw – Exceptions are raised, or thrown, using the construct. Any code in the same code-block that come after a thrown exception are not executed.
- try – Any exceptions generated from the code within the block, can be caught by the following catch {} blocks. Unless the exception is thrown within the try {} block, it cannot be handled by an associated catch {} block.
- catch – catch {} blocks catch exceptions. The code within the catch {} block is used to handle and respond to the caught exception. Some languages require that you explicitly name the exception, while others allow for a generic block.
- finally – finally {} blocks always execute after a try {} block, regardless of whether exceptions are thrown or not.
Also note that an IllegalStateException is thrown that isn't caught by the ExceptionConstructExample() function. The exception will continue to bubble up through the calling functions until one of them catches and handles the exception. If the exception is not caught, it will bubble up to the main program executable which will ultimately result in the program exiting in a failed state.
Exception Abuse
Because exception handling constructs are so powerful and unique in functionality, they can, and are, very quickly and easily abused. Some coders have found clever ways to abuse the exception handling constructs in their respective languages. For example, exception handling constructs can be used as a form of a “GOTO” construct to jump up out of a nested function into a higher function in the call stack, or to terminate from a loop. Exception handling constructs have been used for program flow as well. For example, let’s say that you have written a function that processes a new user account, which throws an exception up to the calling function to determine which view in the user interface is displayed next. Each of these examples illustrate situations where the exception handling constructs are abused.
Poor Practice: A C# example of using exceptions for flow control:
The example is extreme. Let's make the following observations to help understand how the example shows very bad practice:
- Using an exception to control which view is shown next.
- While these are valid exceptions, they are not exceptional exceptions and shouldn't be handled with error handling constructs. These exceptions are detracting from and watering down truly exceptional exceptions. More on this in the next section.
- Using exceptions to control flow created a clumsy construct at the end of the function that wouldn't otherwise be needed.
Exceptional Exceptions: Truly Exceptional
It is from the practice of watering down exceptions with unexceptional exceptions that the term “exceptional exceptions” came from. The rule of thumb that is derived from this line of thought is that “exceptions should be truly exceptional.” An exceptional exception can be simply defined as a condition that should never occur under normal operation. Examples of exceptional exceptions include, for example cases where a database goes down while your program is reading from it, or due to a programming error, a variable expected to have a value at a given time doesn't (null pointer exception).
Three “common” reasons for exceptional exceptions are:
- Programming Errors are exceptions caused by errors in your code. The software engineer has made a mistake, such as using a resource that is null, or passing the wrong parameter into a function.
- API Exceptions are exceptions caused by errors in other software engineer’s code. If you are using a third party library for example, exceptions can be caused by coding errors in that library, or in parameters passed to that API by your code.
- System Exceptions are exceptions caused by hardware or other system-level failures. For example, if a HDD fails, or a network goes down, IO exceptions can occur, or faulty RAM or other hardware can also cause system exceptions.
Unexceptional Exceptions: Watered Down Exceptions
As illustrated in the previous section, “Exception Abuse”, using exceptions for program flow produces clumsy/hacked code, even when it is as well written as the example. Not only is it poor practice, using exceptions for program flow or other abuses, detracts from the severity of the truly exceptional exceptions. An arguably proper use of exceptions is to use exceptions for all errors, ranging from user input errors up to total system failure. However, once a software engineer has implemented such extravagant exceptions, they, and anyone else maintaining the code are soon flooded and overwhelmed with exceptions. When this rush of exceptions is flowing through the system, it becomes very tempting and likely inevitable that the engineer become waterlogged/jaded with the exceptions: the exceptions have been watered down.
With a flood of unexceptional exceptions, truly exceptional exceptions will get buried and ignored. Watering down the exceptions quickly disarms the software engineer, and anyone else involved with the code base, in the battle with truly exceptional conditions. As a result, the engineer now has no tool to differentiate and handle the exceptional exceptions independently of the unexceptional exceptions.
Don't Use Exceptions in Unexpected Places
It is good practice to not use exceptions in unexpected places, unless absolutely necessary; I.E. constructors, destructors and in constructs like Properties in C#. In the case that exceptions are used in a constructor or destructor, you should catch the exception in the same function, and handle it cleanly. If an exception is thrown within a constructor, the objects destructor may never be called. If an exception is thrown within a destructor, or the destructor is never called, the memory will never be freed, causing a memory leak. Additionally, exceptions thrown in Property methods can cause exceptions during construction or destruction. Even in languages with memory management (Like C# and Java), exceptions thrown within construction/destruction, can cause critical cleanup functionality to never be called, resulting with the program to be in an illegal state, or even still, cause managed memory leaks.
Exceptional Exceptions Example
The Java code snippet below showcases all three types of common exceptional exceptions, and a fourth: some unexceptional exceptions. Do not take this snippet as “good code”, it is, in fact a Code Smell:
The following observations are made of the above snippet:
- Programming Exceptions are caused by a programming error where the function is used incorrectly. These are valid exceptional exceptions.
- API Exceptions are thrown by accessing an external API, or by the functions own interface. If the API is programmed correctly, this exception is truly an exceptional exception. In this example, the TradeRatesUnavailableException (Owned by the external API) is wrapped with a RatesUnavailableException (Owned by the local ValidateRate API) and thrown, to maintain encapsulation and cohesion.
- System Exceptions are system-level exceptions. In this example, for some reason, the logging system may unavailable, likely due to resource constraints of the operating system. This is a valid exceptional exception.
- Unexceptional Exceptions will occur during normal operation and in the example, are actually the directive of the function: to validate a proposed rate. These are invalid/unexceptional exceptions: the function should be refactored to use some other form of notification instead of the languages exception handling constructs. In an upcoming part, the “Alternative Solutions for Unexceptional Exceptions” section will introduce some examples of how this can be re-factored to more correctly handle the unexceptional exceptions. An example is provided in the section “Unexceptional Exception Handling Example”
Exceptions are Expensive
Most languages exception handling constructs are expensive to build. In these languages, the exception objects are populated with debug and stack trace information (the stack trace being the list of method calls leading to the exception). Below is an example of a result of the Java snippet from the “Back to Basics: What is an Exception?” section earlier:
Because that exception was towards the top of the program (“main”), there wasn't a large stack trace. Regardless, the system had to build the exception object out and populate it with stack information (all of those “at” lines). Imagine a very complex/deep web based content management system where the call could be tens of functions deep. Regardless of where the error is caught, the system would have to build the entire stack trace, taking valuable resources. The construction of the exception object is much heavier than is necessary for unexceptional exceptions. There are plenty of other, more reasonable options to managing unexceptional exceptions as an alternative to your languages built in exception handling.
Part I Conclusion
In this part, we took a look at what exceptions are in programming and common language constructs for handling exceptions. We discussed the different ways that exceptions could be abused. Lastly, we briefly looked at an example which shows a blending of proper, and improper exception handling.
In the next part, "Part II: Handling Exceptional Exceptions", we will explore some best practices and tips for proper exception use and handling. Stay tuned!
Passionate about people; passionate about software.
9 年I just published "Part II: Handling Exceptional Exceptions": https://www.dhirubhai.net/pulse/exceptional-exceptions-demystified-part-2-ivan-pointer