8.4. Exceptions

 < Free Open Study > 

8.4. Exceptions

Exceptions are a specific means by which code can pass along errors or exceptional events to the code that called it. If code in one routine encounters an unexpected condition that it doesn't know how to handle, it throws an exception, essentially throwing up its hands and yelling, "I don't know what to do about this—I sure hope somebody else knows how to handle it!" Code that has no sense of the context of an error can return control to other parts of the system that might have a better ability to interpret the error and do something useful about it.

Exceptions can also be used to straighten out tangled logic within a single stretch of code, such as the "Rewrite with try-finally " example in Section 17.3. The basic structure of an exception is that a routine uses throw to throw an exception object. Code in some other routine up the calling hierarchy will catch the exception within a try-catch block.

Popular languages vary in how they implement exceptions. Table 8-1 summarizes the major differences in three of them:

Table 8-1. Popular-Language Support for Exceptions

Exception Attribute

C++

Java

Visual Basic

Try-catch support

yes

yes

yes

Try-catch-finally support

no

yes

yes

What can be thrown

Exception object or object derived from Exception class; object pointer; object reference; data type like string or int

Exception object or object derived from Exception class

Exception object or object derived from Exception class

Effect of uncaught exception

Invokes std::unexpected() , which by default invokes std::terminate() , which by default invokes abort()

Terminates thread of execution if exception is a "checked exception"; no effect if exception is a "runtime exception"

Terminates program

Exceptions thrown must be defined in class interface

No

Yes

No

Exceptions caught must be defined in class interface

No

Yes

No


Exceptions have an attribute in common with inheritance: used judiciously, they can reduce complexity. Used imprudently, they can make code almost impossible to follow. This section contains suggestions for realizing the benefits of exceptions and avoiding the difficulties often associated with them.

Programs that use exceptions as part of their normal processing suffer from all the readability and maintainability problems of classic spaghetti code.

—Andy Hunt and Dave Thomas

Use exceptions to notify other parts of the program about errors that should not be ignored The overriding benefit of exceptions is their ability to signal error conditions in such a way that they cannot be ignored (Meyers 1996). Other approaches to handling errors create the possibility that an error condition can propagate through a code base undetected. Exceptions eliminate that possibility.

Throw an exception only for conditions that are truly exceptional Exceptions should be reserved for conditions that are truly exceptional—in other words, for conditions that cannot be addressed by other coding practices. Exceptions are used in similar circumstances to assertions—for events that are not just infrequent but for events that should never occur.

Exceptions represent a tradeoff between a powerful way to handle unexpected conditions on the one hand and increased complexity on the other. Exceptions weaken encapsulation by requiring the code that calls a routine to know which exceptions might be thrown inside the code that's called. That increases code complexity, which works against what Chapter 5, "Design in Construction," refers to as Software's Primary Technical Imperative: Managing Complexity.

Don't use an exception to pass the buck If an error condition can be handled locally, handle it locally. Don't throw an uncaught exception in a section of code if you can handle the error locally.

Avoid throwing exceptions in constructors and destructors unless you catch them in the same place The rules for how exceptions are processed become very complicated very quickly when exceptions are thrown in constructors and destructors. In C++, for example, destructors aren't called unless an object is fully constructed , which means if code within a constructor throws an exception, the destructor won't be called, thereby setting up a possible resource leak (Meyers 1996, Stroustrup 1997). Similarly complicated rules apply to exceptions within destructors.

Language lawyers might say that remembering rules like these is "trivial," but programmers who are mere mortals will have trouble remembering them. It's better programming practice simply to avoid the extra complexity such code creates by not writing that kind of code in the first place.

Throw exceptions at the right level of abstraction A routine should present a consistent abstraction in its interface, and so should a class. The exceptions thrown are part of the routine interface, just like specific data types are.

Cross-Reference

For more on maintaining consistent interface abstractions, see "Good Abstraction" in Section 6.2.


When you choose to pass an exception to the caller, make sure the exception's level of abstraction is consistent with the routine interface's abstraction. Here's an example of what not to do:

Bad Java Example of a Class that Throws an Exception at an Inconsistent Level of Abstraction

 class Employee {

  ...

  public TaxId GetTaxId() throws EOFException {  <-- 1  ...

  }

  ...

} 

(1) Here is the declaration of the exception that's at an inconsistent level of abstraction.


The GetTaxId() code passes the lower-level EOFException exception back to its caller. It doesn't take ownership of the exception itself; it exposes some details about how it's implemented by passing the lower-level exception to its caller. This effectively couples the routine's client's code not to the Employee class's code but to the code below the Employee class that throws the EOFException exception. Encapsulation is broken, and intellectual manageability starts to decline.

Instead, the GetTaxId() code should pass back an exception that's consistent with the class interface of which it's a part, like this:

Good Java Example of a Class that Throws an Exception at a Consistent Level of Abstraction
 class Employee {

  ...

  public TaxId GetTaxId() throws EmployeeDataNotAvailable {  <-- 1  ...

  }

  ...

} 

(1) Here is the declaration of the exception that contributes to a consistent level of abstraction.

The exception-handling code inside GetTaxId() will probably just map the io_disk_not_ready exception onto the EmployeeDataNotAvailable exception, which is fine because that's sufficient to preserve the interface abstraction.

Include in the exception message all information that led to the exception Every exception occurs in specific circumstances that are detected at the time the code throws the exception. This information is invaluable to the person who reads the exception message. Be sure the message contains the information needed to understand why the exception was thrown. If the exception was thrown because of an array index error, be sure the exception message includes the upper and lower array limits and the value of the illegal index.

Avoid empty catch blocks Sometimes it's tempting to pass off an exception that you don't know what to do with, like this:

Bad Java Example of Ignoring an Exception

 try {

   ...

   // lots of code

  ...

} catch ( AnException exception ) {

} 


Such an approach says that either the code within the try block is wrong because it raises an exception for no reason, or the code within the catch block is wrong because it doesn't handle a valid exception. Determine which is the root cause of the problem, and then fix either the try block or the catch block.

You might occasionally find rare circumstances in which an exception at a lower level really doesn't represent an exception at the level of abstraction of the calling routine. If that's the case, at least document why an empty catch block is appropriate. You could "document" that case with comments or by logging a message to a file, as follows :

Good Java Example of Ignoring an Exception
 try {

   ...

   // lots of code

  ...

} catch ( AnException exception ) {

   LogError( "Unexpected exception" );

} 

Know the exceptions your library code throws If you're working in a language that doesn't require a routine or class to define the exceptions it throws, be sure you know what exceptions are thrown by any library code you use. Failing to catch an exception generated by library code will crash your program just as fast as failing to catch an exception you generated yourself. If the library code doesn't document the exceptions it throws, create prototyping code to exercise the libraries and flush out the exceptions.

Consider building a centralized exception reporter One approach to ensuring consistency in exception handling is to use a centralized exception reporter. The centralized exception reporter provides a central repository for knowledge about what kinds of exceptions there are, how each exception should be handled, formatting of exception messages, and so on.

Here is an example of a simple exception handler that simply prints a diagnostic message:

Visual Basic Example of a Centralized Exception Reporter, Part 1
 Sub ReportException( _

   ByVal className, _

   ByVal thisException As Exception _

)

   Dim message As String

   Dim caption As String



   message = "Exception: " & thisException.Message & "." & ControlChars.CrLf & _

      "Class: " & className & ControlChars.CrLf & _

      "Routine: " & thisException.TargetSite.Name & ControlChars.CrLf

   caption = "Exception"

   MessageBox.Show( message, caption, MessageBoxButtons.OK, _

      MessageBoxIcon.Exclamation )



End Sub 

Further Reading

For a more detailed explanation of this technique, see Practical Standards for Microsoft Visual Basic .NET (Foxall 2003).


You would use this generic exception handler with code like this:

Visual Basic Example of a Centralized Exception Reporter, Part 2
 Try

  ...

Catch exceptionObject As Exception

  ReportException( CLASS_NAME, exceptionObject )

End Try 

The code in this version of ReportException() is simple. In a real application, you could make the code as simple or as elaborate as needed to meet your exception-handling needs.

If you do decide to build a centralized exception reporter, be sure to consider the general issues involved in centralized error handling, which are discussed in "Call an error-processing routine/object" in Section 8.3.

Standardize your project's use of exceptions To keep exception handling as intellectually manageable as possible, you can standardize your use of exceptions in several ways:

  • If you're working in a language like C++ that allows you to throw a variety of kinds of objects, data, and pointers, standardize on what specifically you will throw. For compatibility with other languages, consider throwing only objects derived from the Exception base class.

  • Consider creating your own project-specific exception class, which can serve as the base class for all exceptions thrown on your project. This supports centralizing and standardizing logging, error reporting, and so on.

  • Define the specific circumstances under which code is allowed to use throw-catch syntax to perform error processing locally.

  • Define the specific circumstances under which code is allowed to throw an exception that won't be handled locally.

  • Determine whether a centralized exception reporter will be used.

  • Define whether exceptions are allowed in constructors and destructors.

Consider alternatives to exceptions Several programming languages have supported exceptions for 5-10 years or more, but little conventional wisdom has emerged about how to use them safely.

Cross-Reference

For numerous alternative error-handling approaches, see Section 8.3, "Error-Handling Techniques," earlier in this chapter.


Some programmers use exceptions to handle errors just because their language provides that particular error-handling mechanism. You should always consider the full set of error-handling alternatives: handling the error locally, propagating the error by using an error code, logging debug information to a file, shutting down the system, or using some other approach. Handling errors with exceptions just because your language provides exception handling is a classic example of programming in a language rather than programming into a language. (For details on that distinction, see Section 4.3, "Your Location on the Technology Wave," and Section 34.4, "Program into Your Language, Not in It."

Finally, consider whether your program really needs to handle exceptions, period. As Bjarne Stroustrup points out, sometimes the best response to a serious run-time error is to release all acquired resources and abort. Let the user rerun the program with proper input (Stroustrup 1997).

 < Free Open Study >