Exceptions are all over the place in Delphi. Introduced way back in Delphi 1.0, they totally changed the way that folks looked at error handling. Previously, we’d do things like:
ErrorCode := DoSomething;
if ErrorCode > 0 then
We had to constantly interrupt the flow of our code in order to make sure that all is well. Exceptions changed all that by letting you put all the error handling at the end, in a try…except block – out of the way of the flow of your code -- where you could handle things separately, leaving you to worry about how things should go, and not how they might go wrong.
Way back for Borcon 2004, I wrote a paper on exception handling called “Exception Handling for Fun and Profit”. You can read that and get a good idea on the proper way to handle exceptions. It is eight years old, but it’s still all good advice.
What I want to talk about here, though, is the best way to write your own exception classes and exception class hierarchies. The Runtime Library gives you a number of Exception types, and it uses most of them. You are, of course, free to use any of those exceptions declared in SysUtils.pas and elsewhere, and of course you can simply create an exception from the base class Exception. But most often when writing your own library code, the standard exception classes are not enough. So I’m going to try to give you some ideas on how you can and why you might create your own exception classes.
A note before I start: Exception hierarchies should be part of library code; that is, code you write for use by other applications. As noted in my aforementioned article on exceptions, application developers should trap exceptions, and library code writers should raise exceptions. Exception hierarchies are part of developing library code, so this article really only pertains to that side of the fence.
The first thing I’ll tell you is that you should never raise a plain exception:
raise Exception.Create(‘An error occurred’);
I can’t think of any reason for ever doing that. It provides no useful information about the type of exception and why the exception being raised, leaving consumers of your code with little helpful information.
So, as you’ve probably noticed, I like to say “You should never do <this>”. And of course, when I do that, it’s incumbent on me to say what you should do instead. So here goes.
First, you should always raise a specific exception for a specific exceptional situation. If it’s a “normal” exception, use on of the standard Exception classes that are part of the RTL. For instance, if you are dealing with streams, and you have an error, you might raise an EStreamError.
Most often, though, you want your exception to be as precise and exact as possible. This way, it will be easier to pinpoint exactly what went wrong when the exception is captured and displayed. And in order to do that, you should create your own exception hierarchy. If an existing exception type isn’t perfect for the situation, then don’t be afraid to define one that is.
And when you do that, the hierarchy you create should be flat and wide. Flat and wide exception hierarchies allow for the simple organization of similarly purposed exceptions. Here’s what I mean by that.
Flat and Wide
I’ll use my class library THTMLWriter as an example. It it, there is a unit called HTMLWriterUtils.pas that contains the following code:
EHTMLWriterException = class(Exception);
EEmptyTagHTMLWriterException = class(EHTMLWriterException); // Tested
EOpenTagRequiredHTMLWriterException = class(EHTMLWriterException); //Tested
EHeadTagRequiredHTMLException = class(EHTMLWriterException); // Tested
ETryingToCloseClosedTag = class(EHTMLWriterException); // Tested
ENotInListTagException = class(EHTMLWriterException); // Tested
ENotInTableTagException = class(EHTMLWriterException); // Tested
ENotInCommentTagException = class(EHTMLWriterException); // Tested
ENotInFieldsetTagException = class(EHTMLWriterException); // Tested
ENoClosingTagHTMLWriterException = class(EHTMLWriterException);
ENotInFrameSetHTMLException = class(EHTMLWriterException); // Tested
ENotInMapTagHTMLException = class(EHTMLWriterException); // Tested
ENotInFormTagHTMLException = class(EHTMLWriterException); // Tested
ENotInObjectTagException = class(EHTMLWriterException); // Tested
EClosingDocumentWithOpenTagsHTMLException = class(EHTMLWriterException); // Tested.
ETableTagNotOpenHTMLWriterException = class(EHTMLWriterException); // Tested
EParamNameRequiredHTMLWriterException = class(EHTMLWriterException); // Tested
ETagIsDeprecatedHTMLWriterException = class(EHTMLWriterException); // Tested
ENotInSelectTextHTMLWriterException = class(EHTMLWriterException); // Tested
ECaptionMustBeFirstHTMLWriterException = class(EHTMLWriterException); // Tested
EBadTagAfterTableContentHTMLWriter = class(EHTMLWriterException); // Tested
ENotInDefinitionListHTMLError = class(EHTMLWriterException); // Tested
ECannotNestDefinitionListsHTMLWriterException = class(EHTMLWriterException); // Tested
ECannotAddDefItemWithoutDefTermHTMLWriterException = class(EHTMLWriterException); //Tested
This is an example of a “flat and wide” exception hierarchy. The hierarchy is “flat” because there are only two levels of inheritance – the base class and a large number of descendants. A class diagram of these classes would be “flat” because there isn’t much distance from the top to the bottom of the inheritance. It’s “wide” because there are a lot of sibling classes on the second level of inheritance. The class diagram would take a lot of horizontal space if it were laid out on paper.
How does this all work? First, I declared a base type, EHTMLWriterException. All the exceptions raised in the THTMLWriter library will be a descendant of this type. As a result, you can easily check the type of an exception, and if the type is EHTMLWriterException, then you know what library raised the exception. When you are using the THTMLWriter library, you can easily trap all the exceptions raised by the library within your exception handling code by simply calling:
//code that raises an exception in THTMLWriter....
on E: EHTMLWriterException do
//you know that the exception came from THTMLWriter
Second, I declare a large number of very specific descendants that are raised in very specific situations. Their names are very descriptive, and they are usually raised only in one place – the exact place that the error occurred. This makes locating the cause of the exception much easier. Precisely named exceptions can be easily searched for in code.
For instance, here’s an example of some code calling a very specific exception:
function THTMLWriter.OpenParam(aName: string; aValue: string = cEmptyString): IHTMLWriter;
if TStringDecorator.StringIsEmpty(aName) then
Result := AddTag(cParam)[cName, aName];
if TStringDecorator.StringIsNotEmpty(aValue) then
Result := Result[cValue, aValue];
Here you can see that the code raises an exception named EParamNameRequiredHTMLWriterException. The class name alone tells you that it is an error from the THTMLWriter library, and that it was raised because a parameter name was required but not found when the call was made. Just the exception’s class name has a lot of information. In addition, you’ll note that when I raise these specific exceptions, I include a very specific, detailed error string explaining what happened. In my view, you can’t have enough information in an error message.
Thus, if you end up raising this exception, you’ll get this:
Given all that, if you can’t figure out exactly what the problem is and exactly where it occurred, well, then I’m not sure you are in the right business.
Lots and Lots of Exception Types
Since it is fine – and even desirable – for exception hierarchies to be wide, you shouldn’t be shy about declaring many exception classes as direct descendants of your one main exception class. You’ll note above that even in a small library of code like THTMLWriter, I’ve declared 23 exception types. I see nothing wrong with having a large, very specific set of exceptions that you can raise throughout your code library. As I said before, if your exception type is only raised once, that’s fine – it’s very easy to find the location of an exception that is only raised in one place.
So there it is – make your exception class hierarchies flat and wide, use good exception messages when raising them, and you’ll enable the consumers of your code to easily see where problems are arising and to quickly deal with them when they do.