A Flexible Framework For Error Reporting

C/C++ Users Journal December, 2005

Decoupling error generation from error reporting

By Terence Parker

Terence Parker is the founder of Expandable Language (http://www.xlanguage.com/) and produces emulation and development tools for LeapFrog's Leapster and FLY products. He can be reached at ttparker@xlanguage.com.

To report errors effectively, software developers often face this dilemma: Full details of an error are available at the point where the error occurred—often deeply nested within a class or subroutine—but the current application context determines if and how the error should be reported. This problem frequently appears when creating libraries that will be used by a wide variety of applications. While the error-reporting techniques I present in this article apply to all application code, for convenience I frame it in terms of reporting errors from libraries.

Traditional approaches to reporting errors from within libraries include:

Hardcoding Errors. Hardcoding errors is inflexible and limits the usability of your library functions. It often happens when application code is refactored into a shared library, and I have seen cases where GUI error dialogs ended up running in server code. This shows that it is a good idea to use a flexible error-reporting scheme throughout all of your application code.

Returning Error Codes. Returning error codes from library functions suffers a few problems: It is painful to pass an error code back up through several layers of subroutines, which often requires modifying the function signatures of the intervening subroutines; it is easy for clients of the library to overlook or ignore reporting the error. Unless you provide a routine that converts the error codes to strings, end users of applications based on your library may end up with cryptic representations of the error, including much-dreaded messages such as "Error=-259."

Throwing Exceptions. Throwing exceptions when errors are encountered was originally considered a reasonable use for C++ exceptions. Thanks to Herb Sutter's work on the complexity of writing exception-safe code [1], I and many other cautious C++ practitioners prefer to reserve exceptions for truly exceptional/unrecoverable conditions rather than as a means of returning expected failure status. There is also the risk that a client of your library might neglect to write a try/catch block around every call into your library, which is no fun if testing does not find it before the code ships.

Passing Pointers. Passing the library a pointer to an error function is a solution that gets us on the right track. It provides good decoupling between error generation and error presentation. You can adapt the error reporting for both GUI and nonGUI environments or suppress error reporting altogether. However, it suffers from a single function signature, which can limit formatting options. Also, it doesn't track dynamic state, so if you want to temporarily use a different error function, you have to manage that state yourself.

The Solution: Singleton-Based ErrorLogger Class

I have to admit that when I first read about the Singleton pattern [2], I wasn't impressed. I didn't understand the order-of-instantiation problems it can solve and didn't see how it differed much from using global variables.

What I overlooked was the power of polymorphism within the pattern. The Singleton Instance() member function returns a pointer to a Singleton instance. If your Singleton class defines virtual member functions, you can design your Singleton to return instances of derived classes. The decision of which derived instance to return can be based on application state.

This insight was the key to creating a tidy method of decoupling error generation from error reporting. Listing 1 shows a bare bones ErrorLogger class that provides virtual Error() and Log() member functions. (Code for this article is available at http://www.cuj.com/ code/. It has been tested with both g++ v3.3.3 and MSVC6 using 1.32.0 of the Boost libraries, and should be easy to port to other environments.)

I prefer classifying messages into critical "errors" and noncritical "warning/log information," but you can customize your base ErrorLogger class to any level of granularity. The default implementation of these functions streams critical messages to cerr and noncritical messages to cout, which is fine in many nonGUI application contexts.

The ErrorLogger class also includes the static member functions PushLogger() and PopLogger(), which push and pop entries on the loggers_ stack. Subsequent calls to the Instance() member function returns the last-pushed logger. This is the mechanism whereby application code changes error presentation, using these steps:

  1. Derive a class from ErrorLogger.
  2. Override its Error() and Log() functions.
  3. Push an instance of the derived class onto the ErrorLogger Singleton.

This implementation uses the Boost smart pointer classes [3] to protect against the dangling reference and clean-up problems presented by raw pointers. I chose reference-counted boost::shared_ptrs over transfer-of-ownership std::auto_ptrs because applications need to own the ErrorLogger-derived objects they create, especially when an ErrorLogger caches information.

Because I frequently derive from the base ErrorLogger class, I like to keep its interface minimal. I find it useful to use freestanding functions to provide vararg and other detailed formatting of all error strings. These freestanding functions in turn invoke the ErrorLogger with the composed string. Declarations for these helper functions are in Helpers.h (Listing 2).

ErrorLogger in Use

I have successfully used variants of the ErrorLogger class presented here both in my own products and for my current employer. Here is a sampling of ways I have overridden ErrorLogger's logging functions to better control and present application errors.

Filter duplicate error messages. Some procedures produce error information that repeats itself and there is no easy way to avoid it (think of a connect-to-network function that retries several times). Simply set up an ErrorLogger-derived class to cache all messages into an STL set, and when you are done with the procedure, dump the contents of the set. When you try to insert a repeat entry into an STL set, the insertion fails, so you end up with only a single report of each distinct error encountered. Listing 3 shows the CacheErrors class from 2Cache.cpp.

Logging to a file. It is straightforward to fill this classic need. Set up your ofstream object in the constructor of your logger and stream messages into it within the Log() and Error() functions. Listing 4 shows an ErrorLogger-derived class from 3LogToFile.cpp that does this.

Send messages to a debug console. The emulation engines I have worked on were greatly enhanced by redirecting trace/debug output to a console/logging application. I also often debug GUI application code by directing all of the application's informational messages to the debug output window of my development environment. Listing 5 shows a class from 4LogToConsole.cpp that uses the Win32 OutputDebugString() function.

Ignoring classes of errors. Using conventions in formatting your error messages provides lots of control over their later presentation. I find that messages prefixed with square-bracketed-enclosed identifiers are easy to parse and easy on the eye. For example, you can code messages by library/subsystem ([audio], [netaccess], for instance) or by severity level ([1], [2]). Listing 6 shows how to filter messages using an ErrorLogger-derived class from 5Filtered.cpp.

Logging from background threads. Ideally, logging from a background thread is no different than logging from the foreground thread. I have made this goal a requirement for the ErrorLogger class because I use it within library functions and those functions don't know how they are being invoked.

Safety in multithreaded applications requires protecting contested resources by serializing access to them. For the ErrorLogger class, there are two areas that may require serialization:

Your applications may not require serializing access to both sets of functions. For example, if each thread in your application logs to a different file, you don't need to add protection within the Log() or Error() functions, but your Instance() function will need to choose a different logger object (and thus file) based on the current thread ID. New threads need to add their loggers into the mapping used by the Instance() function, so access to those mapping variables must be protected.

Adding Background Logging

Many applications make use of background worker threads that are scheduled by the foreground thread. A background thread provides a distinct stream of information and needs a stable repository in which to place it. The foreground thread may be responding to GUI events and temporarily changing its logging scheme. It is a good idea to make logging for background threads independent and give them their own ErrorLogger object.

Listing 7 shows an updated ErrorLogger class for the scenario where a foreground GUI thread has full control over its background worker threads. It incorporates a new SetBgLogger() method and associated bgLogger_ member variable to track a separate ErrorLogger object for background threads. No serialization protection is provided for this variable—the foreground thread is responsible for modifying it in a controlled manner.

A static boost::mutex member variable helps to serialize access by multiple threads to cerr and cout in the base class Error() and Log() functions, and is available to derived classes as well. Its usage causes the threads' messages to be interleaved line-by-line rather than the free-for-all that can mix output from different threads on the same line.

This updated ErrorLogger implementation and the multithreaded sample code in 6BgThreads.cpp (Listing 8) use the cross-platform Boost Threads Library for their threading and locking primitives. You can read about this well-designed library at Boost [4]. Determining thread identities using this library is a bit awkward because thread objects do not provide an ID, but rather, an equality operator. The ThreadHelper class in Listing 7 uses boost::thread objects to detect whether the current thread is running in the foreground or background.

Conclusion

Error reporting is problematic if you cannot decouple error generation from error presentation, especially within libraries that are used by many clients. Failure to get it right can bubble up into application code and make your library and its client applications much less usable.

By using a Singleton-based ErrorLogger class in conjunction with polymorphism, you can create a simple-to-use, flexible framework for error reporting. This framework makes it possible to generate detailed error information in deeply nested library routines and later filter or transform the presentation of those errors in ways that might not have been anticipated. For example, imagine receiving a new requirement to timestamp every entry into your application's log.

Error reporting is one of those concerns that is widely distributed throughout application code. In the terminology of aspect-oriented programming, it can be considered an "aspect" of your code. Using polymorphism with the Singleton design pattern to solve this problem raises the question of where else this approach might help. I would love to hear from readers the other uses you find for Singletons that change their behavior based on application state.

Acknowledgments

Thanks to Tamar Cohen, Bruce von Kugelgen, and Yolanda Burrell for reviewing this article.

References

  1. Sutter, H. Exceptional C++, Addison-Wesley, 2000.
  2. Gamma, Erich, Richard Helm, Ralph E. Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.
  3. Boost Smart Pointers Library; http://www.boost.org/libs/smart_ptr/smart_ptr.htm.
  4. Boost Threads Library; http://www.boost.org/doc/html/threads.html.
CUJ