This article is part of a longer running series on Error Handling with Exceptions.
Determining where to catch an exception can be one of the most difficult aspects to overcome in becoming comfortable with exceptions. These rules provide clear guidance and simplify the process by removing ambiguity.
Please note that this article’s scope is limited to the location of catch clauses. Questions on the state of the application and its ability to continue in the presence of specific exceptions will be addressed in future articles. The distinction between where to catch and what to do when an exception is caught is important, as this represents one of the most misunderstood facets of effectively working with exceptions.
Where to Catch
- Catch specific exceptions locally if it is possible to mitigate the error and continue
- Catch all exceptions locally if the error is benign within the context of a function’s intended purpose
- Catch all exceptions at the function level when wrapping exceptions
- Catch all exceptions at logical boundaries
Catch specific exceptions locally if it is possible to mitigate the error and continue
Although relatively uncommon, it is possible to automate the correction of an error condition and continue execution. For instance, a function that opens a file may be in a position to prompt the user and wait if an exception associated with a file in use is encountered. In these limited scenarios, it is appropriate to catch specific exception(s), correct the problem, and continue.
void MyFunction(void) { while(true) { try { AFileHandle hFile; hFile = OpenFile(...); // Process file... break; } catch(FileInUseException const &ex) { PromptUser(); } } // Continue... }
Catch all exceptions locally if the error is benign within the context of a function’s intended purpose
There are times when an exception encountered within a function is benign within the context of the function itself. For example, failure to write to a log file may be critical when used for auditing purposes as part of a financial transaction but benign within the context of a function that is initiating emergency shutdown of a nuclear reactor.
It is important to reiterate that at this point in the discussion, it isn’t important to discuss what exception was thrown but rather where it was thrown and what that failure means within the larger context of the function.
Any time a function is called within a function that we are writing, we should ask ourselves “what does a failure here mean to the calling function”. In most cases, failure in a called function indicates failure for the calling function – we should not catch and let the exception percolate. However, if executing the calling function is far greater in importance than successfully completing the called function, we should catch and let the calling function continue.
// Note that this code is not production-quality and is // intended for illustrative purposes only. // Logging is critical to the function's success void DoBankTransaction(void) { Log::Write("Begin transaction"); // Do transaction Log::Write("End transaction"); } // Logging is not critical to the function's success void BeginEmergencyShutdown(void) { try { Log::Write("Begin emergency shutdown"); } catch(...) { /* Log exception */ } // Do shutdown try { Log::Write("End emergency shutdown"); } catch(...) { /* Log exception */ } }
Catch all exceptions at the function level when wrapping exceptions
Exceptions should be caught and converted when implementing a function whose contract specifies that only exceptions of a specific type will be thrown. There are times when it can be convenient to specify that all exceptions that originate from a specific layer within an architectural hierarchy be of a consistent type (note that this convention becomes significantly less important when leveraging techniques to be outlined in “Generic Customization of Exception Messages”). In these cases, all exceptions should be caught and converted within the function itself. Care should be taken to ensure that the new exception thrown contains a reference to the original exception, as this is required when investigating the root cause of the problem while post-mortem debugging.
// Note that all exceptions eminating from this function // will be of type MyFunctionException. void MyFunction(void) { try { // Do something that may throw } catch(PotentialBaseException const &ex) { THROW_EXCEPTION(MyFunctionException(ex)); } catch(std::exception const &ex) { THROW_EXCEPTION(MyFunctionException(ex)); } catch(...) { THROW_EXCEPTION(MyFunctionException()); } // This code is painful to write and maintain and is not // recommended. See the article "Error Handling with Exceptions: // Generic Customization of Exception Messages" for an // alternative. }
Catch all exceptions at logical boundaries
All exceptions should be caught at logical boundaries, where a logical boundary may be an application’s entry point, subsystem boundary within a partitioned architecture (eg the boundary between a COM component and C++ layer), binary boundary, programming language boundary, or a “home screen” within a GUI application.
This article is part of a longer running series on Error Handling with Exceptions.
Fortunately, throwing exceptions is very easy in most languages, and the best recommendation is to throw in such a way that all information required by the exception is populated. When creating custom exception types, care should be taken to ensure that the user of the exception (when throwing) is not placed under undue hardship while populating its required information. The following recommendations help to eliminate such a burden:
- Create a macro if the language doesn’t populate location information regarding where an exception was thrown. This macro should internally calculate the location and add that information to the exception.
- Ensure that exceptions can be created and thrown in a single statement; any difficulty associated with throwing an exception will decrease the likelihood that the exception is used in practice. Consider these code samples:
MyExceptionType ex; ex.SetArg1("Foo"); ex.SetArg2("Bar"); throw ex;
and
throw MyExceptionType().SetArg1("Foo").SetArg2("Bar");
The first sample requires that the user create the exception type and set each value within its own statement. The second example doesn’t prevent that usage pattern, but does facilitate the more concise version.
This article is part of a longer running series on Error Handling with Exceptions.
Fortunately, there are only a couple of things to keep in mind when throwing exceptions (and most of these requirements are built into the language or codified through convention).
What to Throw
- Exception types should be specific to an error
- Exception types should derive from a common base class
- Instances of exception types should contain all information to accurately diagnose the conditions that cased the failure
- Instances of exception types should contain all information to accurately diagnose where the exception was thrown
Exception types should be specific to an error
There should be a 1-to-1 correspondence between exception types and the error that caused the exception to be thrown, such that it is possible for a caller to catch on a specific problem (e.g. an attempt to open a file that is locked by another user) or a more general problem (e.g. an attempt to open a file failed). In some cases, it might be appropriate for a caller to initiate action to correct the error that caused the exception to be thrown, while in others the caller may choose to terminate the current operation and restore the user to a known good state. The end result is that it isn’t the thrower than knows how to continue, it is the caller – and that caller needs the correct contextual information to make informed decisions.
Some languages and platforms place cumbersome criteria on what it considers to be a valid exception type. For example, the following is a minimal C# exception type:
[System.Serializable] public class MyException : System.Exception { public MyException() { } public MyException(string message) : base(message) { } public MyException(string message, System.Exception inner) : base(message, inner) { } protected MyException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } }
In such systems, a foundation should be created (through macros, code snippets, base class implementation, etc) to reduce as much friction as possible when creating new exception types. Any friction not removed will serve as a deterrent to creating exception types specific to the errors that they represent.
Exception types should derive from a common base class
In the vast majority of cases, custom exception types should be a part of well-defined exception hierarchy ultimately rooted in an object defined by the programming language or common library. Exception types not conforming to this recommendation greatly increase the burden placed on the caller by requiring additional catch logic for incongruent types.
For example, in the following code example MyException1 and MyException2 are not based on std::exception, thus placing a requirement on the caller of FunctionThatMayThrow to handle inconsistently rooted exceptions.
struct MyException1 {}; struct MyException2 {}; try { FunctionThatMayThrow(); } catch(MyException1 const &ex) { // Do something with ex } catch(MyException2 const &ex) { // Do something with ex } catch(std::exception const &ex) { // Do something with ex }
Even worse than the burden placed on the caller is that this code is brittle; what happens when FunctionThatMayThrow or a function that it calls is modified such that MyException3 is thrown? Without a common bases class, all exception-aware functions using FunctionThatMayThrow (either directly or indirectly) must be modified to handle MyException3.
The following addresses these problems in that the exception types are based on a well-defined object hierarchy. As a result, the caller can catch by exceptions of a specific type, exceptions in a common hierarchy, or any exception that is thrown.
struct MyBaseException : std::exception {} struct MyException1 : MyBaseException {}; struct MyException2 : MyBaseException {}; // Handle all exceptions try { FunctionThatMayThrow(); } catch(std::exception const &ex) { ... } // Handle MyBaseException and children try { FunctionThatMayThrow(); } catch(MyBaseException const &ex) { ... } // Handle specific exception try { FunctionThatMayThrow(); } catch(MyException1 const &ex) { ... }
Instances of exception types should contain all information to accurately diagnose the conditions that cased the failure
Exceptions should contain enough information to determine what happened to cause the exception.
When post mortem debugging, an exception is oftentimes the only information available to help diagnose the problem on a remote machine. In the absence of a consistent repro scenario or the ability to attach a debugger, the exception must be complete and contain all information necessary to determine what happened. This information may include the name of a file that couldn’t be opened, the name of a socket address that was unexpectedly disconnected, or the name of the variable that was null in the case of a null reference exception. I have found the answer to the following question serves as a good guide in fulfilling this requirement:
Given this exception and nothing more, it is possible to determine exactly what went wrong?
Instances of exception types should contain all information to accurately diagnose where the exception was thrown
Exceptions should contain enough information to determine where a problem originally happened. In most cases, determining where an exception happened means capturing and persisting call stack information at the moment the exception is thrown. This information should be a part of every exception and available when the exception is caught (regardless of where the it is caught or if the exception is wrapped by a new exception).
Many languages and platforms make it very easy to capture call stack information while others do not. At the time of this writing, the following resources are available to generate call stack (or call stack-like) information in C++ applications:
- __FILE__, __LINE__, and __FUNCTION__ preprocessor macros
- Walking the Callstack by Jochen Kalmbach
- [boost] [Backtrace] Any interest in portable stack trace?
This article is part of a longer running series on Error Handling with Exceptions.
It can be a challenge to know when to throw exceptions and, unfortunately, the common guidance to throw in exceptional circumstances doesn’t do much to clarify the confusion. In this article, I will put forward postulates of when to throw exceptions and then explain each of those in more detail.
Note: In this article I will use the term function and method interchangeably. With regards to discussions of exceptions, precision associated with the differences between the two terms is not germane to the discussion.
When to Throw Exceptions
- Exceptions should be thrown when the intended purpose of a function cannot be completed
- When performance characteristics associated with exceptions for a particular function are prohibitive to the function’s use, a second, opt-in “nothrow” function should be created
- “nothrow” functions cannot be created if there is more than 1 potential cause of failure within the function
- The vast majority of mutable member functions should not return a value
- All immutable member functions should return a value
Exceptions should be thrown when the intended purpose of a function cannot be completed
A function should do one thing, and that “one thing” should be captured in the return value (or an object state change if writing a mutable member function). A function’s failure to accomplish its intended purpose implies that a return value cannot be created and should result in an exception. We run into trouble when we attempt to:
Overload a return value with special meaning
// rval < 0 on failure int ReturnPositiveNumber();
In this example, the return value represents a domain that is intentionally larger than the domain of a valid return value. While this does work, our selection of a return value is artificial, is not able to express valid constraints, and may restrict the domain of valid values (for example, what if std::numeric_limits
// Is nullptr valid? MyObject * GetObject();
Again, the validation for this function may be fairly intuitive. However, any ambiguity introduced into code imparts some amount of burden on to the caller, and this burden is likely to lead to bugs when multiple developers are working within a code base.
Eschewing Return Values by Returning Error Codes Some systems require that functions return error codes, where values within the return type’s domain are well known. For example, COM requires that methods return HRESULTs to indicate success or failure. This solution does do a great deal to eliminate ambiguity associated with overloaded return values, but users pay a high price as a result.
HRESULT DoSomethingWithCOM(IUnknown *pUnknown);
When consuming the code above, HRESULTs contain rules that differentiate between values that indicate success and values that indicate failure. However, rigid solutions such as this prevent the return of actual results, meaning that the caller must create temporary variables to store pass as arguments when calling the function (note that it is possible to return successful values that can be overloaded to indicate a function’s result (S_FALSE), but such practices are generally discouraged).
Most troublesome, solutions such as this tend to be viral – it can be very difficult to communicate error codes to callers without returning the error code itself. This can result in a full call stack with knowledge of HRESULTs but no direct dependency on COM whatsoever. Leaky abstractions such as this immediately decrease the likelyhood of maintaining a layered architecture.
When performance characteristics associated with exceptions for a particular function are prohibitive to the function’s use, a second, opt-in “nothrow” function should be created
Sometimes, it makes sense to look before you leap. For example, it probably doesn’t make sense to throw exceptions in the following contrived code example:
for(int iCtr = 0; iCtr < 100000000; ++iCtr) { int iValue(Parse("This is not an int")); // ... }
It may be advantageous to create a function similar to the first that does not throw an exception when invalid input is encountered. Fortunately, the new function does not violate the first postulate as its new name will reflect the intended return value’s purpose.
int Parse(char const *pszString); bool TryParse(char const *pszString, int &value);
While helpful, this technique should be used with care, and these guidelines should be followed:
Psychologically, the function that requires less discipline on the part of the caller (in this case, Parse) should be the more discoverable of the two variations. A user should knowingly opt-in to use a function that requires additional actions on their part (while this is actually accomplished with the name of the function itself, is can be valuable to make the more advanced function require slightly more effort to type). Note that leading a user towards the first function might have a negative impact on performance, but it is better to be correct that it is to be performant.
The default function should always be implemented in terms of the advanced function (DRY: Don’t Repeat Yourself) as in the code below.
int Parse(char const *pszString) { int value(0); if(TryParse(pszString, value) == false) throw std::invalid_argument("pszString"); return(value); } bool TryParse(char const *pszString, int &value) { // Parse, return false if invalid }
“nothrow” functions cannot be created if there is more than 1 potential cause of failure within the function
It is not valid to create a “nothrow” option of a function if there are multiple points of potential failure within a function. It is important for debug-ability to know what error happened, not simply that an error happened. For example, the following function is not valid as there may be multiple reasons why the file could not be opened (file does not exist, file in use, permissions, etc).
bool TryOpenFile(char const *pszFilename, HandleToFile &file); // BAD!
Again, it is better to be correct than it is to be performant.
The vast majority of mutable member functions should not return a value
By definition, a mutable member function is intended to modify its internal state (generally through the modification of member variables). As such, the purpose of the function is to modify the state of the associated instance and not to return a value consumed by an external entity. While not always true, a mutable member function that returns a value should be subjected to various Code Smell investigations to see if such usage is appropriate.
All immutable member functions should return a value
By definition, an immutable member function should always return a value, as the function should not be modifying its internal state as a result of the function’s execution. Therefore, anything that is done by the function should be captured within the return value.
This article is part of a longer running series on Error Handling with Exceptions.
In today’s landscape, there are two primary means of handling errors within an application: exceptions and error codes. Working under the assumption that a clear and consistent error handling strategy is a Good Thing, the following is a brief list of why exceptions are a superior strategy when working within languages that support them.
Note that this article attempts to justify why exceptions represent a superior error handling strategy over the use of error codes. I understand that we are reaching the point where some readers may not have any experience with error codes at all. If you are one of these young pups, feel free to skip ahead.
Exceptions Represent the Path of Least Resistance
In many of today’s popular languages, exceptions are the preferred error handling strategy. This preference may be directly communicated in coding guidelines or implied by a standard/base class library that fundamentally throws (Link 1, Link 2). It is futile to resist the use exceptions under these circumstances (I speak from experience, as I foolishly tried). In some languages, there are means by which a developer can disable the use of exceptions in some specific scenarios, but this action generally has far reaching ramifications and amounts to throwing (HA!) the baby out with the bath water.
When working with a language that throws, it is generally easier (and likely better) to embrace the use of exceptions; a developer will be forced to deal with exceptions anyway, and a mixed-mode error handling strategy is just about as bad as no error handling strategy at all.
Exceptions Lead Towards the Path of Correctness
Any tool or technique that relieves a developer from the requirement of perfection is a good thing; techniques that rely on developer discipline have a higher potential rate of failure than those that don’t. Error handling with exceptions eliminates the need for developer discipline by requiring that error conditions be handled (even if “handled” means an application crash (while such crashes represent unacceptable behavior, it is better than ignoring error conditions (intentional or otherwise))). Most developers can tell the story of broken user experiences caused by an error code dropped in one line of one function called in one place, which resulted in the program continuing, blissfully unaware that something had gone wrong. Exceptions eliminate this entire category of developer error by construction.
Exceptions Facilitate the Creation of Cleaner Code
The following two code examples are functionally equivalent. The first example uses error codes to communicate errors while the second uses exceptions. Even though the examples are contrived, I have experienced the very same increases in code clarity when extrapolating the same modifications over larger code bases.
Error Codes:
// Prototypes HRESULT GetObject(int someCriteria, Object *pObject); HRESULT Function1(void); HRESULT Function2(void); HRESULT MyFunction(int someCriteria) { HRESULT hr; if(FAILED(hr = Function1())) return(hr); if(FAILED(hr = Function2())) return(hr); Object temporary; if(FAILED(hr = GetObject(someCrieria, &temporary))) return(hr); if(FAILED(hr = temporary.Method())) return(hr); // ... }
Exceptions:
// Prototypes Object GetObject(int someCriteria); void Function1(void); void Function2(void); void MyFunction(int someCriteria) { Function1(); Function2(); GetObject(someCriteria).Method(); // ... }
Exceptions Enable the Creation of “Complete” Objects
Errors may be encountered when creating an object. Without exceptions, all operations that the potential to fail are placed in a Construct-like method within a pattern called Two Phase Construction. In this pattern, “safe” operations are placed in the object’s constructor and “unsafe” operations are placed in a second method. It is the user’s responsibility to create the object, call the construct method, check for errors, and discontinue use of the object if errors were encountered during the second phase of construction.
This pattern is problematic because:
- It relies on developer discipline (exerted by the caller) to correctly follow the create/construct/check/discontinue use pattern every time an object is created
- It relies on developer discipline (exerted by the object’s author) to correctly check for a properly initialized object in every method
Fortunately, exceptions deprecate this pattern in that all construction activities can be placed in the constructor; potential errors are propagated to the user in the form of an exception. The result of this is that an object is either fully created or it isn’t; the caller isn’t required to make additional calls and the object’s author isn’t required to check for meaningless state information).
There is no good reason to use the Two Phase Construction Pattern in languages that support exceptions.
Exceptions can Contain Contextual Information
Unlike error codes, exceptions can be created to contain rich contextual information about the error and when and where it happened. This information may be about the cause of the error (the name of the file that couldn’t be opened), about the location in which the error was encountered (the call stack when the exception was thrown), or conditions that contributed to the error (caught exceptions that contributed to the current error (child exceptions)). While such mechanisms are possible with error codes (Link 1, Link 2), they are custom, often kludgy, rarely thread-safe, and more cumbersome to use than their exception counterparts.
Exceptions are Hierarchical
Unlike most error codes, exceptions can be organized into logical hierarchies. These hierarchies greatly increase a developer’s ability to selectively continue operations when error conditions are encountered. For example, some designs may dictate that one hierarchy of exceptions are fatal while another are not. By catching by base class rather than specific exception type, the design is open to future enhancement. Such hierarchical organization is extremely difficult to achieve with error codes.
A series of articles regarding error handling with exceptions.
- Error Code Fail
- Why Exceptions
- When to Throw
- What to Throw
- How to Throw
- Where to Catch
- What to Catch
- What to do With a Caught Exception
- The Strong Exception Guarantee
- Testing for the Strong Exception Guarantee
- Generic Customization of Exception Messages
- Windows and SEH Exceptions
- Miscellaneous
- References
This article is part of a longer running series on Error Handling with Exceptions.
The following books and articles have been invaluable over the years as I have investigated exceptions and exception-related issues. In most cases, they have stated things far better than I could ever hope to.
Articles
Abrahams, David. “Exception-Safety in Generic Components“ http://www.boost.org/community/exception_safety.html
Wagner, Bill. “Working Effectively with (.NET) Exceptions“ http://reddevnews.com/Articles/2009/08/01/Working-Effectively-with-Exceptions.aspx
Books
Meyers, Scott (1996). More Effective C++. Addison Wesley Longman, Inc.
Sutter, Herb (2000). Exceptional C++. Addison Wesley Longman, Inc.
Sutter, Herb (2002). More Exceptional C++. Addison Wesley Longman, Inc.
Sutter, Herb (2005). Exceptional C++ Style. Addison Wesley Longman, Inc.
Sutter, Herb and Alexandrescu, Andrei (2005). C++ Coding Standards. Addison Wesley Longman, Inc.
Wagner, Bill (2005). Effective C#. Pearson Education, Inc.
This article is part of a longer running series on Error Handling with Exceptions.
Right around 2000, I began to explore error handling via exceptions. There were many things I liked about exceptions; as a designer, I could:
- Ensure that users processed error conditions
- Provide rich contextual information along with the error itself
Unfortunately, I wasn’t at all comfortable with the introduction of jump/goto-like semantics when exceptions were introduced. In my naive-ate, I decided that I was going to create a solution that leveraged the best of both words.
I made good progress as I attempted to create an error code-based alternative to exceptions. Despite this progress I quickly realized that I was fighting a losing battle. The code that I was producing, while correct, was cumbersome to use – if I was having a hard time using it how could I expect success while evangelizing its use. Complexity resulted from the requirements that return codes must be composable and be convertible to a function’s result type. Worse, I realized that the end result of this effort was precluding the use of something that I had previously grown to love – the Standard Template Library. Finally, the killer was that the C++ new operator throws std::bad_alloc instead of returning null upon failure. By moving forward, I was swimming against the current of the entire C++ community and ultimately destined for failure.
It became overwhelmingly clear that it wasn’t possible, practical, or even prudent to try to fight the use of exceptions any longer. With this in mind, my focus changed to the evaluation and understanding of exceptions. This led to ultimately embracing exceptions as a superior means of error communication and propagation. I can honestly say that this change in perspective has had a profoundly positive impact on the designs that I produce and the quality of the code that I write. After literally hundreds of hours of thought, investigation, and experimentation, I hope to share some of what I learned along that journey.
In the coming days/weeks, I hope to write on the following topics. Please feel free to post in the comments if there is anything that you would like to see in these blog entries or specific questions that you may have on the topic.
Fully understanding and embracing exceptions is hard work, but my experience shows that the rewards greatly outweigh the cost.
After time away from blogging, I have decided to spend some time here and there writing about things that I am actively working on. I have really enjoyed a number of articles that have appeared on dzone.com over the past couple of months and hope that I can contribute to the discussion where appropriate.
In the near future, I hope to write on a variety of different topics including software architecture, exception handling, criteria to aid in the evaluation of designs, random C++/C#/Python tidbits, and anything and everything else that may or may not be interesting.
I can’t promise that this will be an entertaining or particularly useful site, but I do promise to provide updates far more regularly that what you have seen from me in the past.
Thanks for reading!