JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 14 "Exceptions and Exception Handling"

Updated 04.28.2007

In this chapter, I am going to ramble about and suggest one alternative system for declaring, using and handling exceptions. I will then go on to discuss checked exceptions, design by contract, validation pre-conditions and assertions.

What is an Exception?

The first fundamental question is "What is an exception?" or "When should an exception be thrown?" In the past, exceptions were said to signal an exceptional condition, not routine errors. David Abrahams has argued that one person's exception is another person's error, making the distinction less than useful. 

http://www.boost.org/more/error_handling.html

Abrahams argues "An oft-cited guideline is to ask yourself the question 'is this an exceptional (or unexpected) situation?' This guideline has an attractive ring to it, but is usually a mistake. The problem is that one person's 'exceptional' is another's 'expected': when you really look at the terms carefully, the distinction evaporates and you're left with no guideline. After all, if you check for an error condition, then in some sense you expect it to happen, or the check is wasted code."

In a recent article in the C/C++ Users Journal (August 2004), Herb Sutter has argued for a strictly defined domain of an "error" and the use of exceptions over error codes. 

Herb Sutter concludes "Distinguish between errors and non-errors. A failure is an error if and only if it violates a function's ability to meets its callees' pre-conditions, to establish its own post-conditions, or to establish an invariant it shares responsibility for maintaining. Every thing else is not an error...

Finally, prefer to use exceptions instead of error codes to report errors. Use error codes only when exceptions cannot be used ... and for conditions that are not errors."

In this chapter, I suggest a modification of Herb Sutter's strict approach to the exception domain to remove the ambiguity in determining what is exceptional behavior.

In my limited experience, I sometimes find error codes more useful and at other times I find that exceptions are more useful. For instance, I prefer Boolean return values or return error structures when I call functions in a dependent sequential series. In this case, using Boolean return values makes coding less complex and easier to read. At other times, throwing an exception on error seems more appropriate. For instance, in a complex function with a single common "exit on pre-condition error" point, it is easier to wrap a series of exceptional function calls in a try catch construct and provide a way to exit the function in a single pre-condition error catch clause. Any system that attempts to systematically define when a method should throw an application exception and when a method should return an "error code" needs to address this dichotomy.

Runtime vs Application Exceptions

At the start, I should point out that there are really two basic types of exceptions, runtime exceptions and application exceptions. In this chapter I am trying to define when it is appropriate to throw and catch an application exception. Runtime exceptions are used to signal fatal errors and are generally not handled locally by an application. Instead, resource allocation calls to methods that may throw runtime exceptions are wrapped in a try finally construct. Resources are reclaimed in the finally cause and the runtime exception is passed up the call chain to a global runtime exception handler.

A Twisted Approach to When a Method Should Throw an Exception

In this chapter I am going to try to argue that the explicitly stated pre-conditions, invariants and post-conditions of a method should determine if a method should throw an exception or return an "error code" on failure. This approach supports the use of both exceptions and errors, but removes any ambiguity about the caller's responsibility. Specifically, by perusing the documentation, the caller should be able to determine if it should verify a pre-condition, check for an error, or trap for a specific exception.

A concrete example will help to explain the dilemma of what is an exceptional event. A common "well known" application exception is the FileNotFoundException thrown by the File methods. Users of many File methods are generally expected to code defensively. For instance, it may be wise to call File.Exist before opening a file. Alternatively a user could simply wrap the call to open a file in a try catch construct and trap for a FileNotFoundException. In a single threaded, single user system, the first approach may be preferred. In a multi-threaded or multi-user environment the second approach may be superior since it is possible for a second process to delete the file "almost simultaneously."

Note: Checking for a method requirement prior to invoking the method has been described as the Tester-Doer idiom.

In a similar matter, if an explicitly stated invariant is violated then the method should throw an exception. A concrete example would be a thread safe abstract AccountBase class with a Withdraw method with a critical section of code that locks the account balance. In a multi-threaded environment it is not possible for the caller to validate that the account balance is adequate for the withdrawal outside of the critical section of code. If the method explicitly asserts the invariant "withdrawal <= balance" then the method should throw an exception if the invariant is violated.

// ASSERT INVARIANT withdrawal <= balance

If the documentation does not explicitly document this invariant then it should not throw an exception if withdrawal <= balance. The method could return false.

In this chapter, I argue that the decision to return an error code or throw an exception in a method that opens a file is dependent on the explicitly stated pre-condition of the method. If a pre-condition of a method that opens a file is that the file must exist, then the method should throw an exception if the file does not exist. Alternatively, if there is no stated pre-condition that the file must exist then the method should return an "error code" on failure to find a file. Alternatively, the method might return null on failure to find a file.

The beauty of this approach is that it explains why one designer might decide to throw a FileNotFoundException whilst another designer might decide to return a Boolean error structure for a given method on failure. The "method context" or stated pre-conditions determine the appropriate way for a method to handle a failure. An API could provide support for both "method contexts." In keeping with .NET 2.0, a method named MyMethod could throw an exception on failure, whilst a method named TryMyMethod could return false. Of course any method that can throw an exception should explicitly document the pre-condition that would result in throwing an exception on failure.

Note: .NET 2.0 recognizes that not all methods should throw exceptions on error. For instance, the TryParse methods return false on error and use the "out" semantics to return a result.

Validate, Trap or Check

So far we have looked at exceptions from the perspective of the called method.  We can also look at this problem from the perspective of the client or caller of the method. For a given pre-condition the caller of the method has three choices:

bulletValidate the pre-condition
bulletTrap for an exception if the pre-condition is violated
bulletCheck for a return error code

If a method explicitly states the pre-condition then the caller should only have two choices:

bulletValidate the pre-condition
bulletTrap for an exception if the pre-condition is violated

If it is possible for the caller to completely validate the pre-condition, then I would argue that caller should just validate the pre-condition and not trap for the pre-condition exception.

In a similar manner, for a given invariant the caller has two choices:

bulletTrap for an exception if the invariant is violated
bulletCheck for a return error code

If a method explicitly states the invariant then the caller can trap for the invariant exception or allow the exception to percolate up the exception chain. 

Checked Exceptions

Some languages like Java  implement checked exceptions, providing a built in mechanism for enforcing local exception handling using the throws keyword. In Java, the compiler enforces the application exception handling contract between the supplier and the client. The designers of C# decided to avoid adding checked exceptions to the language. Anders Heljsberg, the lead C# architect, has argued that checked exceptions lead to problems with scalability and versioning. This in turn, this tempts coders to just catch all exceptions locally and then ignore them. Here is a link to an Anders Heljsberg interview:

http://www.artima.com/intv/handcuffs.html

In this interview, Heljsberg argues that adding a new checked exception to a published method, breaks backward compatibility. Any existing client code will break since it does not explicitly catch the new checked exception. To code defensively in a rapidly changing environment with checked exceptions, a defensive coder might be tempted to swallow all exceptions in his calling code like this catch{}. In a similar manner, Heljsberg argues that checked exceptions do not scale well. As method chains increase, the number of aggregate checked exceptions increase, again tempting the coder to just swallow all exceptions in the calling code. Due the problems of versioning and scalability, the designers of C# elected not to include checked exceptions. 

Pre-Conditions and Design By Contract

Pre-conditions are used to statically verify that input parameters and conditions are valid before executing an algorithm or action. Pre-conditions can be verified by the client, the supplier, or by both the supplier and client. For instance, if the supplier is a C++ ATL COM component that is going to be used across architectures such as JavaScript, C++ or C#, it may be best if both the client and supplier (COM component) check for pre-conditions. In  mission critical applications where reliability is paramount over efficiency, both the client and supplier may also want to verify the pre-conditions.

If efficiency is important and the client and supplier are well "contained" by the design team, the pre-conditions may be checked exclusively by the client. This is the argument for design by contract (DBC). In DBC, the client is expected to validate the input and enforce the pre-conditions. The supplier is expected to validate the output and enforce the post-conditions. In the release or retail mode, the supplier does not do any pre-condition validation so that a no-check version of the supplier is used. In debug mode, the supplier may want to throw fatal run-time exceptions or use assertions to signal that the client is violating the DBC. At the cost of some efficiency, the supplier could also choose to log pre-condition violations in the release or retail version.

Validation Pre-Conditions

One approach to inter-method communication is called validation pre-conditions in which the client and supplier agree on a method of communicating pre-condition violations and the client agrees to register and interest in this communication.

Some have argued for validation pre-conditions in which the client and supplier agree on a interest in this communication. In this scenario, it is possible to even consider enforcing pre-conditions exclusively in the supplier. In order to loosely couple the client and supplier, there would need to be a generic method of communicating pre-condition violations to the client. One way to do this, would be to define a PreConditionException.

NoThrow

Sometimes a method should not throw an exception. In the no-fail or nothrow guarantee the method always succeeds!

Transactional Constructors

Since constructors do not have a return value, the only viable way to exit a constructor on error is to throw an exception. In C++ and PHP, if an exception is thrown in a constructor the object never existed and the destructor will not be called. In C++, if you try to allocate unmanaged resources in the constructor and the construction does not complete, you are responsible for releasing any unmanaged resources before exiting the constructor with a throw. In other words, construction should be transactional

C++ and PHP coders will be surprised to learn that in C# even if an exception is thrown in the user defined constructor, the user defined destructor will still be called. 

In a nothrow framework, you can trap for fatal errors and set a member bool flag isValid to false. Alternatively, you can use static factory methods that can return null if you cannot construct a valid object at runtime.

Debug Assert

If you just want to trap for internal logic errors while in debug mode, consider using System.Diagnostics.Debug.Assert. For instance, if you want to catch an indexing error during development, you can call Debug.Assert.

Debug.Assert(this.currentIndex>= 0 && this.currentIndex < arrayElements.Count,
    "Internal Logic Error",
    "Invalid currentIndex in VerticalPanel");

This will launch an error dialog if (this.currentIndex is < 0 || this.currentIndex is >= arrayElements.Count). You can use this code to find internal logic errors in your code during development. This line of code will not execute in release mode.

In Conclusion

My suggestion is that an exception should be thrown when an explicitly stated pre-condition, post-condition or invariant is violated. The corollary is that any pre-condition, post-condition or invariant that may engender an exception must be explicitly stated and documented.

David Abrahams has argued that "the problem is that one person's 'exceptional' is another's 'expected'..." Herb Sutter has argued that any violation of a method's pre-conditions, invariants or post-conditions should result in an exception, removing this ambiguity. I have suggested using a more flexible version of Herb Sutter's concept of an exception domain that  supports both exceptions and errors without ambiguity.

Have fun,
Jeff

Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1