Unchaining Chained Exceptions in C++

Chained exceptions don't always allow you to get at all the information you need. Paolo Brandoli demonstrates a way to preserve the data from the original exception, while maintaining a chain of exception handling.


July 25, 2006
URL:http://www.drdobbs.com/cpp/unchaining-chained-exceptions-in-c/191100567

I was writing a new library in C++ and I needed some kind of chained exceptions facility to better handle the errors generated by my code. Chained exceptions are useful because they allow you to have a sort of stack dump at the moment when the error occurred, and some implementations may also include the state of the functions listed in the stack dump.

A chained exception is created when a function doesn't let an uncaught exception go but catches it and wraps it in a new exception (the chained exception) that is thrown in place of the original one. The new chained exception also contains some information related to the place where the catch & throw happened, and sometimes other information that may be useful to the programmer.

The first catch statement that doesn't panic and is able to deal with the abnormal condition can analyze the content of the chained exception and determine the chain of functions that generated it; as noted before, it's almost a stack dump of the moment when the error occurred.

The Problem

The problem is that the exception being caught is not the original one: if an exception is not processed immediately, then it is lost, or it can be caught only by using special techniques (like catching the chained exception and then analyzing its content).

For instance, have a look at Figure 1: It represents the code of four functions that are used to decode an image file. There is a codec manager, which calls the jpeg decoder which in turn calls the huffman decoder.

Figure 1: Four functions with catch and throw macros.

The code doesn't use the chained exceptions; all the functions have a catch and throw macro, which catches the uncaught exceptions and re-throws them without modifications.

The huffman decoder sooner or later has to read some bytes from the input file, but something goes wrong because the file reader detects an end of file and throws an EOF exception. Then the following events happen:

As you can see, the EOF exception is not processed immediately, but only when it reaches the codec manager; all the other functions don't know how to deal with it.

If the code was using chained exceptions, it could not simply try to catch the EOF in the codec manager, because at that point the EOF was wrapped deep inside a chain of chained exceptions.

But are the macros only catching and throwing the exceptions? Do they just caress the exceptions before they are thrown into the darkness? Perhaps they also keep track of the place where the catch and throw happened, and perhaps they also remember some other information, so a catch statement can analyze the stack dump of the moment when the original error happened, just like the chained exceptions do.

The Solution

I had to decouple the information about the catch and throw points from the exception being thrown. Because each exception is processed by the thread that throws it, I decided to store the catch and throw points in a list coupled to the active thread.

This means that each time an exception is caught and thrown, then the information related to the catch and throw point are added to a list coupled to the active thread and the original exception is thrown again, unmodified.

The first catch statement that is able to handle the error can then retrieve a list of all the points traversed by the exception by analyzing the current thread's "catch and throw" list.

The Implementation

The source code provides two macros to be placed at the very beginning and at the very end of the application's functions, and a statically allocated object called exceptionsManager that logs all the information related to the catch and throw points, organized by thread.

The two macros that have to wrap the functions that need to be monitored are called FUNCTION_START and FUNCTION_END; they wrap the body of the functions in a try block.

FUNCTION_START also allocates a constant string that stores the function's name, while the macro FUNCTION_END contains the catch block that catches all the exceptions uncaught by the function. The catch block calls a method in the exceptionsManager object to store all the relevant information and then re-throws the caught exception.

The information stored in the exceptionsManager includes: the function name, the file name and the line number where the catch and throw takes place, the exception's type, and the exception's message (if any). All the information is piled up until a call to exceptionsManager::getExceptionInfo() is made.

getExceptionInfo() is usually called from the catch block that can process the exception; the function returns a list containing the information about all the catch and throw points traversed by the caught exception and also clears the list in the exceptionsManager.

Usage

The body of each function that needs to be monitored must be enclosed by the FUNCTION_START and FUNCTION_END macros. The FUNCTION_START macro also requires a parameter that represents the function's name; it will be used later by FUNCTION_END if a catch and throw happens.

The catch block that is able to process the exception must have a call to exceptionsManager::getExceptionInfo() or exceptionsManager::getMessage(). Both are static methods.

The first method returns a list of exceptionInfo objects that store the information of the catch and throw points traversed by the exception, and the second method returns a string with a text representation of the objects returned by getExceptionInfo(). Both the methods clear the information list, so subsequent calls to getExceptionInfo() or getMessage() will return an empty list or an empty string.

When throwing an exception, the application's code should use the macro FUNCTION_THROW (although is not necessary), which also logs some information related to the place where the original throw happened.

The library can be compiled on Windows (you have to define the preprocessor symbol WIN32) and on Posix systems.

Example

The complete source code includes the exceptionsManager class, the related macros and a simple console application that executes an integer division between two parameters.

When an exception is thrown, then all the points traversed by the exception are logged and finally displayed on the screen by the outer catch statement. You can try the application and make it fail by specifying less or more than two parameters, or trying to divide by zero.

Listing One shows two functions that include the exceptionsManager macros, and Listing Two shows how the exception can be managed and how to retrieve the stack dump of the caught exceptions.

Listing One

#include "exception.h"

//...

int function1(int left, int right)
{
  FUNCTION_START("function1");
  if(right==0)
  {
    throw std::runtime_error("Ops, dividing by 0!");
  }
  return left/right;
  FUNCTION_END();
}

int function0(int left, int right)
{
  FUNCTION_START("function0");
  return function1(left, right);
  FUNCTION_END();
}

Listing Two

try
{
  return function0(param0, param1);
}
catch(std::runtime_error&)
{
  std::cout << "An error occurred. Stack dump:\n";
  std::cout << exceptionsManager::getMessage();
}

Example 1 shows a typical output of the stack dump.

Example 1.

c:\>cuj.exe 10 0

An error occurred. Stack dump:


[function1]
 file: c:\sourcecode\cuj\cuj.cpp  line: 25
 exception type: class std::runtime_error
 exception message: Ops, dividing by 0!

[function1]
 file: c:\sourcecode\cuj\cuj.cpp  line: 28
 exception type: class std::runtime_error
 exception message: Ops, dividing by 0!

[function0]
 file: c:\sourcecode\cuj\cuj.cpp  line: 37
 exception type: class std::runtime_error
 exception message: Ops, dividing by 0!

Conclusion

The exceptionsManager class and the related macros achieve the same results as chained exceptions, but without the problems introduced by chained exceptions.

You don't have to change your code; your catch statements can continue to look for the same kind of exceptions. There are no exceptions wrapped inside other exceptions, and everything works as it did before.

While it is true that you have to insert the two macros in the functions that have to be monitored, this is also true with chained exceptions, so the overall amount of effort is comparable in the two approaches.


Paolo is a C++ software developer specializing in image analysis and medical image formats. He distributes his products through his web site www.puntoexe.com, and can be contacted at [email protected]

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.