Channels ▼
RSS

Revisiting Exception Handling


May 2003/The (B)Leading Edge



Let me begin this column by going back to my IndexedFile library. I have continued, off and on, to work on that library. One reader pointed out a bug, which I fixed. I also made a number of small, but significant changes to the interface. As a result, the library has gotten easier to use — at least I think it is easier to use — as well as more reliable. It now seems quite solid, enough so that I am looking to use it in production code. Finally, taking my changes together with continuing improvements in compilers and libraries now means that I have reached the point where my tests actually compile and run correctly on several platforms. As a result, I have made available a new copy of the source code. This can be found in the CUJ code archives at <www.cuj.com/code/archive.htm> as part of the November 2002 code collection. Included in this code is a ReadMe file that explains the changes and provides some simple examples on how to get started using the IndexedFile library.

Now let me go back to reviewing my guidelines for exception handling in C++. In the last column, I reviewed my very first "column," which offered guidelines for handling exceptions. My second and third columns were guidelines for throwing exceptions and using exception specifications respectively. Rather than actually review the contents of those two columns point by point, I want to take a broader perspective and look — again — at the overall question of how to effectively use exceptions. This starts with asking the question: "why do you throw an exception?" This seems like a trivial question, with a trivial answer: "you throw an exception when you detect an error," but it seems much more complicated than that to me.

Most software is written in layers. At the top is something that passes for an application. At the bottom are usually operating-system services. In between are thick and thin layers of what is usually referred to as middle-level libraries. Let us take a look at exceptions in the context of one of these middle-layer libraries. For a number of reasons, I like to use containers for my exception examples, preferably something like the STL map container. In this case, I am going to use my own IndexedFile library.

Recall that IndexedFile is built on top of my XDRStream library, which in turn is very similar to the IOStream library from the Standard C++ library. While my IndexedFile examples provide a concrete version of XDRStream classes that are based on the existing concrete IOStream classes in the Standard C++ library, real world XDRStream classes would most likely be implemented by doing direct binary I/O using the low-level operating-system functions available on a given platform. For the purposes of this review, let us consider IndexedFile as consisting of three layers: the top layer provides the Btree index mechanism and the data storage facility, both implemented in terms of XDRStreams. In the middle is the XDRStream library, which in turn is implemented using the facilities from the Standard C++ IOStreams library, usually in conjunction with lower-level I/O facilities. At the bottom layer are the Standard C/C++/POSIX libraries, or their equivalents on some other platform.

Now, let's think about some of the possible errors that can occur when using IndexedFile. At the very lowest level, we might fail to create a new file. Maybe the filename is incorrect, or maybe the user doesn't have the correct permissions. We can get an error when we try to open IndexedFile: maybe the file doesn't exist, maybe it is in use, or maybe the permissions are again wrong. We can get errors writing a file, and we can get errors reading a file. All of these are IndexedFile errors, but they are errors that are detected by the bottom layer of the implementation. My question is: which of these errors should result in an exception? If you think even one of these deserves an exception, then the next questions are who throws it, and what gets thrown?

To me, these questions are the heart of the issue of how to effectively use exceptions in C++. The simple answer is "throw an exception for everything." I have always rejected this answer, but I have to concede that it does have its merits. One of the reasons that I wholeheartedly supported exceptions being in C++ from the beginning is that I have continually seen the problems of trying to build a consistent and reliable error-handling strategy without exceptions. I used two words there: consistent and reliable. They both need explaining. The consistent part should be fairly obvious to anyone who has tried to combine different libraries in the C arena. Before exceptions in C++, the defacto standard method of signaling an error was as a function return. Even though I say defacto, it wasn't especially standard. Some libraries returned zero for success, and non-zero meant an error. Some libraries were the opposite. I refer to these as the Ints and the Bools (like the Hatfields and the McCoys), fundamentally the same, but diametrically opposed. Besides indicating success or failure, there was the problem of providing details on which error occurred (C libraries use errno). Finally, the whole scheme had a problem when you needed to use a function's return value to return a real result. You can build a consistent error-handling scheme using the C style, but it will be for your own libraries. The chances that it will differ from, or even conflict with, some other library are basically guaranteed. If you never need to use any of those other libraries, or you never expect anyone who uses those other libraries to ever want to use your library, then you can get away with this type of thing. Unfortunately, it all strikes me as similar to what would happen if every car company decided independently on which side of the vehicle to put the steering wheel.

Reliability is even more of a problem with traditional C style error-handling schemes. Without exceptions, no matter what approach you take, you have to depend on the client to check the error indication. It can be argued that doing these checks is just part of programming; that skipping a check is similar to forgetting to initialize a variable — maybe you get away with it some times, but sooner or later you get caught. To some extent I agree, but the problem is that reliability collides with consistency, and neither allows for the ability to upgrade. I may do the necessary tests, but unless my code is designed to pass on the same information as was passed to me, then I have a problem. This problem becomes acute when the underlying library changes and new error conditions are added. As programs get larger and are composed of more and more pieces from varied libraries, it becomes easier and easier to understand why so many programmers take the easy way out and just call abort when they encounter an error.

Calling abort, either directly or via an assert statement, is certainly a consistent and reliable way of error reporting; it just doesn't allow much in the way of error handling. So we come to exceptions. In the process, we also come to the idea that the simplest, most consistent, and reliable thing to do when an error is detected is throw an exception. So what is wrong with this idea?

My argument in the past was that an exception should represent a failure in the software specification, or to use Bertrand Meyer's term the contract. I still think this is correct. Unfortunately, you can define your software to signal errors using some mechanism other than exceptions, in which case you do not need exceptions — at least for those errors. Put another way, there is nothing that says you have to define certain conditions as exceptional, instead you might define some conditions as normal. My standard example of this is looking up a key that does not exist in IndexedFile. How else would you know whether the key exists in the index other than to look it up? Most people would agree that something other than an exception should result in the case of a key-not-found condition. Another example is an end-of-file condition. At the bottom level of IndexedFile's implementation, this too would hardly be considered the appropriate place to throw an exception. After all, all files have an end, and usually the only way you find it is by reading up to it. This is not true for IndexedFile however. In IndexedFile, encountering an EOF in a low-level read indicates something is seriously wrong with the structure of IndexedFile itself. As these two examples show, it is difficult to come up with any broad guidelines about what type of conditions (or what type of libraries) should throw exceptions; it depends on the condition and on how the client expects to handle the condition.

This last point has annoyed me over the years. If I expect a condition to happen, whether it is an end-of-file or a key-not-found, then I want to be able to incorporate the handling of that condition in my normal flow of control. What I do not want to do is write code like this:

try
{
    val = isam.find(key);
} catch (IndexedFile::KeyNotFound& er) {
    // ...
}

This strikes me as silly at best. Obviously, IndexedFile does not work that way. Equally obvious, I do not want to be checking for XDRStream end-of-file conditions when I am working with IndexedFiles. I am perfectly happy to have that type of error throw an exception. Unfortunately, someone else working with XDRStreams is likely to want end-of-file to not throw an exception. End-of-file may seem like a bad example, so what about file-not-found on an open. This seems like a reasonably good candidate for an exception, but at the lowest level is it really all that different from key-not-found for IndexedFile?

I recognized the existence of this problem in my original guidelines. If you throw exceptions for every non-normal condition, you basically turn exceptions into just another control mechanism. This is a bad idea because try-catch blocks are a lot harder to read than if-else statements. On the other hand, if you are not going to use exceptions to signal errors, then you end up back in the same boat you have with C-style error reporting and propagation techniques. What I recommended at the time was that libraries should use exceptions to report errors as their default, but they should also provide alternatives that would avoid the exceptions for those cases and those clients that needed to handle the conditions as part of normal control flow. I cited two examples of this from the standard library: the different forms of new (new and new(nothrow)) and the two ways of indexing a string or a vector (the index ([]) operator and the at function). I think the advice was good, but trying to apply it in practice has turned out to be more difficult than I expected. I had a couple of examples of this dual interface in early versions of IndexedFile, but I redesigned the interface to get rid of them. Mostly, I feel the result is better, but part of me wonders.

And so we come to the heart of the matter. In everyday coding, programmers do not want to deal with exceptions. When you see code that is littered with try-catch blocks, you know that somewhere there is some poorly designed code. Originally, I was somewhat disappointed that the standard library did not make more use of exceptions than it did. I have now come to appreciate why it did not. As a rule, the more general purpose a library, the more reasonable it is for it to treat many types of conditions as expected. These should not throw exceptions, but must be reported in more conventional ways. Unfortunately, by the same token, designers of libraries that are intended to be low level and general purpose have little incentive to provide the exception throwing alternatives. This means that most of the libraries that ordinary developers see regularly — and are likely to mimic in their own designs — do not use exceptions as a single, consistent error-signaling mechanism.

As we move up the functionality tree and libraries provide higher levels of abstraction (e.g., IndexedFile versus XDRStream), it becomes more reasonable that low-level errors be converted into exceptions. At the application level, every error can be an exception, but of course at that point it really doesn't matter.

So, I am left with the very unsatisfactory feeling that in practice exceptions have not given us a unified error-handling mechanism. Instead they have just provided another tool that gets mixed — often indiscriminately — with the already existing techniques. I think the guidelines I proposed back in 1996 are all still valid, but they require a certain level of discipline to apply. Unfortunately, what I see in the real world is a mix-and-match hodgepodge of approaches to using exceptions. I see low-level libraries that throw exceptions where I think they should not, and I still see a lot of high-level libraries that don't use exceptions where they should. I like to think this will all filter out in time, but I am beginning to wonder.

One thing that I touched on back in 1996 still seems true to me today — if you encounter an error condition and you don't know what to do about it, then throw an exception. This may not be the best approach, but it at least ensures that the condition will not go unnoticed. If you later find that the condition needs some less drastic treatment, then you can consider adding a more traditional approach to reporting it.

To wrap things up, I will touch briefly on some final guidelines. One is the issue of what exceptions to throw. In my experience, I have found that the standard library exceptions that fall under the category of (and are derived classes of) logic_error turn out to cover the vast majority of the exception conditions that I encounter. For this reason, I usually do not find it either necessary or appropriate to define a class- or library-specific exception type. I also find it somewhat annoying when I encounter some library that defines its own version of domain_error or invalid_argument and nothing else. The lazy programmer in me says "why bother?" I know that this is a contradiction of one of my previous guidelines, but experience says go with the standard exception classes if you can.

Finally, I must add a paragraph or so about exception specifications. When I read over them now, I think my column on using exception specifications was really pretty good. Even today, I encounter people who are religious about them, and I encounter people who cannot understand why C++ does not enforce them at compile time like Java does. (I don't bother trying to explain it anymore.) The vast majority of people whom I know and respect who have tried to use them correctly have found them to be a waste of time. There are other writers and other works that have dealt with this, so I will just summarize my personal experience:

  1. Exception specifications and templates don't mix. Leave them off of template functions, which naturally include any member functions of class templates.
  2. Exception specifications and inheritance don't mix. Leave them off of any virtual functions.
  3. Exception specifications and callback functions do not mix. Granting that object-oriented design usually prefers using objects like Observer or Strategy rather than callback functions, the principle remains the same: leave exception specifications off functions that invoke callbacks.
  4. Exception specifications and the future don't mix. This is because they are not checked at compile time. Changes in underlying implementations can break exception specifications, but the result will not be apparent until what was once working code suddenly starts to die with unexpected exception conditions. Likewise, attempting to combine different libraries that define their own exception classes and rigorously enforce their use via exception specifications can be a real pain.

In practice, I have found exception specifications are not worth the trouble, so I now recommend that you just avoid them altogether — I do.

Well that is just about it. As I said in my previous column, for the first time in six years I do not have an idea for my next column that I feel strongly enough about to write up. One problem is that I have tried hard to focus "The (B)Leading Edge" on how to use Standard C++ and its library in the real world. I noted in the beginning that I did not intend to offer a tutorial, nor was I interested in exploring obscure features of the language/library just for the sake of exploring features. Instead, I really believed that features many people might consider obscure and needlessly complex were in fact useful — if used appropriately — in real-world programming situations that most programmers would encounter sooner or later. That is still my focus, and while there are parts of the library that I have not explored (valarrays being an obvious one), and probably features of the language that I haven't used, nothing that I am currently working on gives me any need to use those features.

Furthermore, I never wanted "The (B)Leading Edge" to be a "C++ gotchas" collection. I wrote a number of such columns when I felt that the issues were important. In particular, a number of columns focused on problems with undefined behavior in various areas. This was and is an issue that I see daily in my work, so I know it is a problem. Yet I also understand the problems and needs of language and library implementers, so I have tried to point out the problem areas and at the same time offer reasonable explanations for their existence when and where I could.

Unfortunately, in the last few months I have encountered a number of gotchas that have left me shaking my head. Some of them I have determined (at least to my own satisfaction) to be errors in compilers. Some I honestly believe are errors in library implementations. Others seem to be possibly legitimate differences in interpretation of some part of the Standard. Finally, a few seem to just be plain quirks in the language itself. All of them have annoyed and frustrated me because they forced me to rewrite what seemed to be perfectly good code for no good reason. There were no "lessons learned" in the process, other than the timeless one of it always takes longer and is harder than expected.

Finally, in 2003, the C++ Standard will be opened for review and revision. Part of me really wants to be involved in that process. I still think C++ is one of the most powerful and useful programming languages ever developed, and I really think I could make some contribution. On the other hand, the cynic in me wonders. For numerous personal and practical reasons, I have found it harder and harder to keep up with the leading edge of the C++ development community of the last couple of years. Maybe I am getting burned out. Or maybe I just need to step back and get focused again.

In any case, I have decided to wrap up "The (B)Leading Edge" and put it in mothballs for awhile. Most of the time authors put their acknowledgements in the front of their works. Somehow, it seems more appropriate for a columnist to put them at the end. In that vein, I would like to thank everyone who has bothered to read this column over the years for taking the time to do so — I hope you got something from it even if it was just confirmation that you already "knew that." I would especially like to thank those who have taken the time to point out errors or oversights in my code or in my reasoning.

I would like to say a special "Thank You" to the editors that I have had the privilege of writing for: Doug Schmidt, Robert Martin, and Herb Sutter at C++ Report and Marc Briand and Joe Casad at C/C++ User's Journal. Finally, I must say a very special "Thanks for Everything" to my managing editors Seth Bookey at C++ Report and Amy Pettle at CUJ.

Lastly, I want to thank what I call the C++ inner community. It is not a small group, but it is not all that large either. It begins with my fellow columnists here in the C++ Experts Forum and prior to that at C++ Report. They have been a constant source of knowledge and inspiration. It has been an honor to be part of that community. Beyond them are the people who helped create C++ in the first place, and taught the first and second generation of users how to use it appropriately. Obviously, at the very top of this collection is Bjarne Stroustrup. I assume that most readers of this column are C++ fans, but whether you love it or hate it, you have to admit that C++ has been responsible for pushing the envelope of programming technique for the vast majority of the industry. A very special thanks goes to Bjarne, and his colleagues at Bell Labs, for giving us C++ in the first place.

About the Author

Jack W. Reeves is an engineer and consultant specializing in object-oriented software design and implementation. His background includes Space Shuttle simulators, military CCCI systems, medical imaging systems, financial data systems, and numerous middleware and low-level libraries. He currently is living and working in Europe and can be contacted via jack_reeves@bleading-edge.com.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video