Amazon.com Widgets On the Proper Design of Exception Hierarchies

On the Proper Design of Exception Hierarchies

By Nick at February 19, 2012 22:13
Filed Under: Delphi

Introduction

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 
begin
  DoNormalStuff;
end else
begin
  UhOh;
end;

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 Basics

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:

type
  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:

try
  //code that raises an exception in THTMLWriter....
except
  on E: EHTMLWriterException do
    //you know that the exception came from THTMLWriter
end;

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.

An Example

For instance, here’s an example of some code calling a very specific exception:

function THTMLWriter.OpenParam(aName: string; aValue: string = cEmptyString): IHTMLWriter;
begin
  CheckInObjectTag;
  if TStringDecorator.StringIsEmpty(aName) then
  begin
    raise EParamNameRequiredHTMLWriterException.Create(strParamNameRequired);
  end;
  Result := AddTag(cParam)[cName, aName];
  if TStringDecorator.StringIsNotEmpty(aValue) then
  begin
    Result := Result[cValue, aValue];
  end;
end;

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:

image

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.  Winking smile

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. 

Conclusion

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.

Comments (7) -

2/20/2012 3:55:01 AM #

Interesting. What is your take on a generic Exception<T> type that is raised within a specific T?

Jeroen Pluimers Netherlands |

2/20/2012 9:54:58 AM #

Jeroen --

That sounds like a really interesting idea -- if each type could have it's own "personal" exception, that would be very useful, I think.

Nick Hodges United States |

2/20/2012 7:12:17 AM #

Hi Nick,

Appreciate your views on "flat and wide" (haven't had chance to dig back to the older article yet though...)

What are your views on exceptions when the exceptions are almost unique, in other words they are never going to be repeated elsewhere in code. Or, put that another way, "flat and extremely wide"?

I've tended to settle for the sort of handling below... The main thing I dislkie about my method is the necessity of repeating the class / method name in the exception handler - in this case "LoadAggregationNode". I see picking up on RTTI as a way of making this more generic, but I've never gotten around to bottoming that one out as yet. So, in essence my method is to use the generic exception, but number the instance of it within any routine and report any relevant values that will help debugging. The other main drawback is renumbering exceptions within a routine if you need to insert one. Nothing says they have to be sequential or even numeric of course, I just like to keep them straight. Generally I don't have a need to pick up on specific exception classes in the exception handler as the resolution to them is normally the same - most are so fundamental that processing cannot continue - assertions almost.

try
   raise Exception.CreateFmt('1. Could not locate summary, GID : %d',[fGIDSum.AsInteger]);
except
      on E:Exception do
      begin
           Query.Active := false;
           E.Message := Format('LoadAggregationNode. Error : %s',[E.Message]);
           raise;
      end;
end;

Paul United Kingdom |

2/20/2012 9:50:59 PM #

I'm a big fan of debugging tools/exception reporting tools. So I very rarely find myself pasting stuff like class names into my exception messages. Instead, MadExcept says "at line 263 TClass.Method raised EProgrammerError" and what I care about is the attached logfile and screenshot. So I try to throw exceptions with the detail focused on parameters and local variables.

So I throw "expected parameter aClass to contain a class reference, but it contained $0*0FA0" or "TMyEnum.SometimesWeLikeBlue (2) does not work here" with an InnerException of ERangeCheckError. Yes, it's possible that the customer will see that message, but ... ENaughtyProgrammer.

I spend more time wondering how to deal with nasty situations customers put me in, where the exception flow gets to a certain point and grinds to a halt. What to do when MadExcept can't send an email, can't FTP out, I can't write a log file anywhere obvious, and the unhandled exception count is 10 since the app was started? Show a message to the user and try to persuade them to paste a screenshot into an email?

Moz Nepal |

2/20/2012 9:57:22 PM #

Also, "except on E:Exception LogException(E); end;" in every method is an antipattern that deserves its own name. And possibly its own circle of hell.

Moz Nepal |

2/21/2012 7:11:37 PM #

Depends.  In server code, that's often the best way to handle it.  (Not necessarily in every method, but that's the general pattern...)

Mason Wheeler United States |

2/23/2012 5:14:46 PM #

Yeah, as long as each instance has been thought through. It's when that is the generic "I have thought about exceptions, see, I handle all of them" that it becomes a problem.

Mostly for callers - I have no idea what (or even if) something has gone wrong down the call stack, making error handling a joke. I call something, it seems to work, but the effects of the call don't appear. There was an exception but it was "handled" so I never saw it.

It's also surprisingly hard to debug code when the symptoms are "missing rows in the database, if we enable logging on the client PC we get occasional exceptions in the log file, but the user sees nothing". All I've been able to do is peel away successive "exception handlers" until the method that actually causes the problem sees the exception. Then I can start trying to work out what the actual problem is.

Moz Nepal |

Pingbacks and trackbacks (2)+

Comments are closed

My Book

A Pithy Quote for You

"The hardest thing in the world to understand is the income tax."    –  Albert Einstein

Amazon Gift Cards

General Disclaimer

The views I express here are entirely my own and not necessarily those of any other rational person or organization.  However, I strongly recommend that you agree with pretty much everything I say because, well, I'm right.  Most of the time. Except when I'm not, in which case, you shouldn't agree with me.

Month List