Ever wonder what happened to that lost exception? Traceable exceptions to the rescue!
An Exceptional Problem
Writing highly reusable software components within a framework often requires deferring the logging of errors and exceptions to the higher application-specific levels of a program. However, the quest for reusability in production systems is often at odds with the critical need for error tracing. This is especially true with exception handling. A framework of reusable components can bubble up an exception for an application to log and handle, but the important information regarding the exception origin point and handling path are usually unknown. This is further complicated by the fact that the original exception may be lost if, during the exceptions ascent, it is filtered by being caught and then re-thrown as another exception type.
A Traceable Solution
Traceable exceptions can solve these problems. Traceable exceptions are exceptions that carry information about the throw locations and exception types in the form of an exception information stack. An exception information stack can be viewed much like a stack dump received through a debugger when viewing a core file. The difference is that the exception stack contains a data entry for each point in the exception handling propagation where the exception is created, thrown, and re-thrown.
Benefits
The power behind combining exception handling with a stack construct can be exploited for a variety of benefits:
- The original exception type is identifiable along with the file, method, and line number where it was declared. Each handling point where the exception is caught and re-thrown is also preserved providing the propagation trace path.
- If the original exception is filtered into a different exception type, the exception stack is passed on to the filtered exception. This allows trace information to be preserved across exception type changes during exception ascent.
- Applications can delay the logging of exception events until higher application-specific processing levels. This strengthens the ability to write reusable software components that dont burden the user with a preconceived logging approach.
- The stack construct allows the reporting and processing of the exception data to operate on the entire exception stack as one package of information at one point in the program. This allows the reporting point to easily maintain the correlation of the different exception entries with a single exceptional event. This also provides performance benefits as reporting occurs at one point instead of at each stage in the exception ascent as in traditional logging.
- Third party exceptions, such as CORBA exceptions, can be handled generically while the exception is propagating up through intermediate exception neutral components. Then the original exception can be recreated and thrown at a higher level (such as at a CORBA interface). An example of this type of exception recovery using CORBA system exceptions will be discussed later in this article.
Traceable Exception Architecture
Figure 1 shows some of the top branches of a sample traceable exception class hierarchy. All traceable exceptions ultimately derive from the CommonExcep base class. This base class provides the internal implementation for the exception stack and the corresponding exploitation methods that are inherited by all derived exception classes.
CommonExcep and ExcepInfo Classes
The CommonExcep base class and the ExcepInfo class are the key components of the traceable exception architecture. Listing 1 shows these classes significant elements.
The CommonExcep class contains the internal STL container for exception information storage. Although a stack is the best logical view of this information, an STL deque (which is a variation of a queue) is the actual implementation container used. A deque allows easy access to the original exception from the bottom of the stack without unloading the rest of the stacks contents. (Referring to a deque as a stack may at first seem like a deception, or worse a mistake, but I have found that talking about an exception deque often produces no recognition of the tracing concept, while referring to an exception stack draws an immediate association with a calling stack and a common understanding of its purpose.)
Each entry in the stack is an instance of the ExcepInfo class. ExcepInfo represents an information reference point to where an exception is handled (i.e., created, thrown, or re-thrown). It is a container class composed of the following reference information:
- An identifier string that uniquely identifies the exception. This identifier is returned by each derived exception class via its overloaded id method inherited from the CommonExcep class.
- The filename and method name strings along with the integer line number for identifying the specific point where the exception was created, thrown, or re-thrown.
- An optional error-message string to describe additional details of the exceptional condition.
- An optional integer-based error code field.
- An optional sub-identifier string that can be used in conjunction with the identifier to control exception class explosion when wrapping third-party exceptions. This field allows a single more generic exception identifier to carry a more specific sub-type of exception. It is provided for pragmatic reasons, and its usage is discussed later in this article.
The three most important CommonExcep methods used in the generation and tracing of exceptions are the protected CommonExcep constructor, the public push method, and the importPrior method.
The CommonExcep protected constructor accepts a derived exceptions unique identifier string along with the required arguments specifying the filename, method name, and line number for the exceptions creation point. It also accepts the optional arguments consisting of the error message, error code, and sub-identifier. The constructor uses this data to construct an ExcepInfo object, which becomes the first entry pushed on its internal stack. A derived exceptions constructor simply calls this protected constructor passing in its unique identifier and all the required and optional arguments.
The push method is called on an exception object from within a catch block. The push method also accepts the required arguments of filename, method name, and line number, which indicate the location where the exception is being handled. This push method creates another ExcepInfo object and pushes it on the internal exception stack. Following this, the exception object may be re-thrown.
The importPrior method imports the exception stack from one exception object to another. It is used primarily during exception filtering when an exception of one type is caught, but another exception type is created and thrown from the catch block. The importPrior method is called on the newly declared exception object, and the caught exception object is supplied as the primary argument. As a result, the caught exception stack entries are copied over to the new exceptions stack in the original sequence and stack position. This allows the original exception stack information to be preserved during exception redefinition and filtering.
Simple Usage Example
Using traceable exceptions is very easy. Lets assume that the doWork method from a WidgetA class is designed to detect a specific error condition and to throw an InvalidDataExcep exception as a result. Listing 2 shows this method declaring an InvalidDataExcep object and throwing it.
Listing 2 also shows the WidgetB class startWork method that calls the doWork method on a WidgetA object. To propagate the InvalidDataExcep object, the WidgetB class simply calls the push method on the caught exception and re-throws it.
In addition, Listing 2 shows an application-level method catching all traceable exceptions and, in this case, simply logging them.
Figure 2 shows a raw stack dump of the trace information that would result from the WidgetA doWork method throwing an InvalidDataExcep exception. The output shows the original exception entry first followed by subsequent entries. The exception origin point and the propagation path are clearly shown.
Usage Considerations
There are several exception handling considerations to discuss from this example. The WidgetA doWork method employs a very specific exception-handling approach. This method first catches third-party specific exceptions, which it converts to traceable exception types and throws. Next, it catches the expected specific InvalidDataExcep exception and simply re-throws it (since it was created locally and already contains the required trace information). Finally, unexpected exceptions are caught and handled.
It is possible to have a more generic approach to a methods catch blocks, which is the approach taken by the WidgetB startWork method. This method also converts third-party exceptions to traceable exception equivalents that it throws. Then, it simply catches all CommonExcep-derived exceptions in a single catch clause, pushes its handling point trace information onto the caught exceptions, and then rethrows them. C++ preserves the original derived exception type even though the base class is referenced in the catch clause. This catch clause ensures that no traceable exception leaves the method without being traced.
In fact, if the WidgetA doWork adopted the generic exception-handling approach, the InvalidDataExcep catch clause (and any other specific traceable exception catch clauses) would be replaced by the single catch clause specifying the CommonExcep base class. One item worth noting with this modification to the doWork method: since both the construction of an exception and the push method create an ExcepInfo entry on the exception stack, there would be two entries for the InvalidDataConfig exception from the originating doWork method. This second stack entry from the originating method is redundant. To address this, the push method always checks the previous stack entry to see if the filename and method name match the previous entry on the stack. If so, the push method does not create a new stack entry but simply returns. The validity of this lies in the fact that the real area of interest for tracing an exception is its method entry point. An exceptions entry point is either its creation point for exceptions generated within the method, or the first catch block of the method that encounters an outside exception.
There is a tradeoff for unexpected exceptions. Unexpected exceptions can be thrown as received, so as not to interfere with the possibility of a higher-level component recognizing and handling the exception. However, it should be noted that doing so will result in a break in traceability for this component for unknown exceptions. Alternatively, the exception can be filtered into a traceable UnknownExcep object, which is thrown.
Creating Traceable Exceptions
The traceable exception classes shown in Figure 1 are just the starting point for defining a traceable exception hierarchy. The derived traceable exceptions can be extremely thin classes whose main purpose is to provide a unique type through their class definition. For this type of exception, the derived class simply needs to provide three methods.
- A public constructor.
- An overloaded id method to return its unique exception string identifier.
- A protected constructor for its own derived classes to call that can be chained up through the hierarchy to invoke the CustomExcep protected constructor.
Derived exceptions may also extend the minimal CommonExcep base class by adding more specific exception information and specialized methods. An example of this is the CorbaSystemExcep class shown in Figure 1. It adds a throwCorba public method and some internal data members that specify a CORBA minor code and completion status.
Different methods may be employed to produce a unique exception string identifier required by the id method. The simplest method is to return the class name of the exception (which should include a namespace qualifier). This could be generated by coding the identifier string into the class method, or by the more sophisticated technique of using the C++ RTTI (Run-Time Type Information) to retrieve the unique class name. The RTTI method has the advantage of ensuring that there will be no mistaken identifier name clashes.
Wrapping Third-Party Exceptions
Tracing is available only within CommonExcep-derived exception classes. Because of this, third-party exceptions need to be translated into traceable exception counterparts in order to trace them. At first, this may seem like a daunting task. In reality, it is not as hard as it sounds. There are three different techniques that can be used to accomplish this.
- Create individual traceable exception counterparts for all specific third-party exceptions that are actually encountered through interfacing with the third-party software.
- Create a single broader traceable exception to provide traceability for a group of third-party exception types and use the exceptions sub-identifier field for distinction between individual third-party exception types within the group.
- Use a combination of the first and second techniques where appropriate.
The first technique for creating individual traceable exception counterparts is the most robust method (and the purer object-oriented approach). Each of the third-party exceptions can still be easily distinguished and handled individually if needed based on the traceable exception type. The main drawback is that there may be a significant number of third-party exceptions to wrap.
The second technique defines a single base traceable exception that represents a group of related third-party exceptions. This single base traceable exception would still have a unique identifier for the exception group. The sub-identifier field is then used to capture the unique third-party exception identifier actually being thrown within the group. The benefit is that a traceable exception class explosion can be mitigated. The main disadvantage with this approach is that the ability to easily distinguish between the individual third-party exception types is compromised (although still possible by looking at the sub-identifer field). Also, any unique individual exception information will be lost as the exception is reduced to a sub-identifier string.
The third technique is a combination of the first two techniques. In this approach, a single base traceable exception is defined that represents a particular group of related exceptions. Specific traceable exception counterparts are then derived as needed from the created base exception where the framework requires the third-party exception distinction. This technique can allow minimal work to initially wrap a set of third-party exceptions, but can grow to encompass the evolving needs for type distinction. The maximum benefit for this approach hinges on how well the initial hierarchy breakdown is done and the real degree and rate of evolution it must ultimately endure. (Of course, this really applies to building anything in software!)
CORBA Example
The traceable exception architecture in Figure 1 shows a limited example of the third technique discussed for wrapping CORBA third-party exceptions. The traceable CorbaExcep base class is used to generalize all CORBA exceptions. Deriving from this initial base class are two more specific traceable base classes that segregate CORBA system exceptions from CORBA user exceptions.
In a particular framework, it might be reasonable to initially expect the need for only a few derived exception types under the CorbaSystemExcep class. This design decision might be supported on the basis that the framework being built only needs to distinguish between a subset of important CORBA system exceptions. All others could be represented by the more generalized CorbaSystemExcep class.
On the other hand, the designers might expect to create individual traceable exception classes derived from the CorbaUserExcep class for all the CORBA user exceptions defined in the frameworks IDL interfaces. Since the framework IDL is defining the specific CORBA user exceptions, it is reasonable to expect that the framework will need to distinguish between the types in handling them.
Third-Party Exception Recovery
In many cases, it is possible for a traceable exception to retain enough information to recreate the corresponding original third-party exception. This can be a very useful feature.
The CorbaSystemExcep class shown in Figure 1 illustrates this capability. This class includes two additional member variables that represent the CORBA standard minor code and completion status for a CORBA system exception. It also adds a throwCorba method to its public interface. The two additional member variables allow the original CORBA system exception to be reconstructed according to the CORBA standard. This is done via a call to the throwCorba method, which recreates the original CORBA system exception and then throws it.
There are many ways to take advantage of the recoverable CorbaSystemExcep class. Framework components can bubble up a CorbaSystemExcep traceable exception (via the CommonExcep catch clause) without any specific CORBA awareness. However, a top-level CORBA interface method can specifically catch the CorbaSystemExcep object, recreate the original CORBA system exception, and throw it across the CORBA interface by simply calling the throwCorba method on the exception object. The original exception is recovered while still preserving the ability to trace the exception origin point and propagation path.
Conclusion
Traceable exceptions are most effective when the software trace coverage is maximized. Towards this goal, every possible method should minimally have a default outer try/catch block with a CommonExcep catch clause that invokes the push method.
Traceable exceptions are not complicated structures, but I have found them to be enormously beneficial in preserving the critical error-tracking information required for exceptional conditions in large-scale production systems. Not only do they prove more robust than traditional exception logging approaches, but they also promote the generation of reusable components and frameworks by allowing cohesive application-level trace reporting and generic exception handling.
Richard Nies is currently a lead software developer/architect for Harris Corp. He has worked for the last 17 years developing software products including large-scale C++ production systems involving: weather analysis, high-speed data delivery, embedded real-time communication, image processing and exploitation, and client/sever information access. He has a MSCS from Louisiana Tech University. He can be reached at rnies@harris.com.