Chapter 16. Error Handling

This chapter contains the following sections:

Thinking about error handling is like anticipating root canal work it's a messy, often unpredictable, and painful topic, one we prefer to avoid. Yet think about error handling we must, in order to write robust code.

The Rogue Wave class libraries all use the same extensive and complete error handling facility. In this model, errors are divided into two broad categories: internal and external. Internal errors, which are further classified as either recoverable or non-recoverable, are due to errors in the internal logic of the program. As you might expect, they can be difficult to recover from and, indeed, the common default response is to abort the program. External errors are due to events beyond the scope of the program. Any non-trivial program should be prepared to recover from an external error.

The next section presents a table that summarizes the Rogue Wave error model. The sections following the table discuss the types of errors in more detail.

The Tools.h++ Error Model

The following table categorizes and describes errors in the Tools.h++ error model. You can use the table as a quick overview, and return to it as a reference.

Table 16-1. Comparison of Error Types in Tools.h++

Error Type

Cause

Examples

Predictable?

Cost to detect

Level of Abstraction

Where detected

Response

Internal Non-recoverable

Faulty logic or coding in the program

Bounds error; inserting a null pointer into a collection

Yes

High

Low

Debug version of library

No recovery mechanism

Internal Recoverable

Faulty logic or coding in the program

Bounds error in a linked list; attempt to use an invalid date

Yes

Low

Low

Debug and production version of library

Throw an exception inheriting from RWInternalErr

External

Events beyond the scope of the program

Attempt to write a bad date, or to invert a singular matrix; stream write error; out of memory

No

Low

High

Debug and production version of library

Throw an exception inheriting from RWExternalErr, or provide test for object validity


Internal Errors

Internal errors are due to faulty logic or coding in the program. Common types of internal errors include:

  • Bounds errors;

  • Inserting a null pointer into a collection;

  • Attempting to use a bad date.

All of these errors should be preventable. For example, you always know the permissible range of indices for an array, so you can probably avoid a bounds error. You would correct your program's use of a bad date as an obvious logic error.

Internal errors can be further classified according to the cost of error detection, and whether or not the error will be detected at run time. The two categories are:

  • Non-recoverable internal errors;

  • Recoverable internal errors.

Non-recoverable Internal Errors

Non-recoverable internal errors share the following distinguishing characteristics. They are:

  • Easily predicted in advance;

  • Encountered at relatively low levels;

  • Costly to detect;

  • Detected only in the debug version of the library.

Non-recoverable internal errors by definition have no recovery mechanism. Examples of these errors include bounds errors, and inserting a null pointer into a collection.

Why does a library define some errors as unrecoverable? Because detecting errors takes time. For performance reasons, a library demands some minimal level of correctness on the part of your program, and pitches anything that falls short. Errors are non-recoverable in the sense that the production version of the library has no mechanism for detecting such errors and, hence, no opportunity for recovering from them.

Bounds errors are non-recoverable because the cost of checking to make sure an index is in range can well exceed the cost of the array access itself. If a program does a lot of array accesses, checking every one may result in a slow program. To avoid this, the library may require you to always use a valid index. Because a minimum level of correctness is demanded, non-recoverable errors are simple in concept and relatively easy to avoid.

You can best discover and eliminate non-recoverable errors by compiling and linking your application with the debug version of the library. See "The Debug Version of Tools.h++" for details. The debug version includes lots of extra checks designed to uncover coding errors. Some of these checks may take extra time, or even cause debug messages to be printed out, so you will want to compile and link with the production version for an efficient final product.

If the debug version of the library discovers an error, it typically aborts the program.

Recoverable Internal Errors

Recoverable internal errors are similar to their non-recoverable relatives in that they are easy to predict and occur at low levels. They differ in that they are:

  • Not costly to detect;

  • Detected in both the debug and the production versions of the library.

A bounds error in a linked list or an attempt to use an invalid date are both examples of recoverable internal errors. The library's response to these errors is to throw an exception inheriting from RWInternalErr.

The production version of the library can check for recoverable internal errors because the cost is relatively low. For example, to find a bounds error in a linked list, the cost of walking the list will far exceed the cost of detecting whether the index is in bounds. Hence, you can afford to check for a bounds error on every access.

If an error is discovered, the library will throw an exception inheriting from RWInternalErr, as we have mentioned. Here's what it looks like when Tools.h++ throws an exception:

// Find link "i"; the index must be in range:
RWIsvSlink* RWIsvSlist::at(size_t i) const
{
   if (i >= entries()){
     if(RW_NPOS == i)
       RWTHROW( RWBoundsErr( RWMessage( RWTOOL_NPOSINDEX)));
     else
       RWTHROW( RWBoundsErr( RWMessage( RWTOOL_INDEXERR,
              (unsigned)i,
              (unsigned)entries()) ));
   }
 
   register RWIsvSlink* link = head_.next_;
   while (i--) link = link->next_;
   return link;
}

In this code, note how the function always attempts to detect a bounds error. If it finds one, it throws an instance of RWBoundsErr, a class that inherits from RWInternalErr. This instance contains an internationalized message, discussed in "Localizing Messages" in Chapter 16. The RWTHROW macro is discussed in "Error Handlers."

Throwing an exception gives you the opportunity to catch and possibly recover the exception. However, because the internal logic of the program has been compromised, most likely you will want to attempt to save the document you are working on, and abort the program.

External Errors

External errors are due to events beyond the scope of the program. As we mentioned in the introduction, any non-trivial program should be prepared to recover from an external error. In general, external errors are:

  • Not easily predicted in advance;

  • Encountered at more abstract levels;

  • Not costly to detect;

  • Detected in both the production and the debug versions of the library.

Examples of external errors would include: an attempt to set a bad date, such as 31 June 1992; an attempt to invert a singular matrix; a stream write error; being out of memory. Tools.h++ would respond to these by throwing an exception inheriting from RWExternalErr, or providing a test for object validity.

External errors may be run time errors. In an object-oriented environment, run time errors frequently show up as an attempt to set an object into an invalid state, perhaps as a result of invalid user input. The example given above, initializing a date object with the nonexistent date 31 June 1992, is an external run time error.

External errors often take the form of exceptions thrown by the operating system. Tools.h++ takes responsibility for detecting these exceptions and recovering the resources it has acquired; it will close files and restore heap memory. As the user, however, you are responsible for all resources acquired by code external to the Tools.h++ library during these kinds of exceptions. Generally, Tools.h++ assumes that these exceptions will not be thrown during memory-related C library calls such as memcpy. Tools.h++ make every effort to detect throws which occur during operations on Tools.h++ objects or user-defined objects.

In theory, the Tools.h++ response to an external error is to throw an exception, or to provide a test for object validity. It should never abort the program. In practice, however, some compilers do not handle exceptions, so Tools.h++ provides an opportunity to recover in an error handler, or to test for a status value. Here is an example of using the isValid function to validate user input:

RWDate date;
while (1) {
   cout << "Give a date: ";
   cin >> date;
   if (date.isValid()) break;
   cout << "You entered a bad date; try again\n";
}

Exception Architecture

When an exception is thrown a throw operand is passed. The type of the throw operand determines which handlers can catch it. Tools.h++ uses the following hierarchy for throw operands:

xmsg
RWxmsg
RWInternalErr
RWBoundsErr
RWExternalErr
RWFileErr
RWStreamErr
xalloc
RWxalloc

As you can see, the hierarchy parallels the error model outlined in "The Tools.h++ Error Model." This hierarchy assumes the presence of class xmsg, nominally provided by your compiler vendor. This is the class now being considered for standardization by the Library Working Group of the C++ Standardization Committee X3J16 (Document 92-0116). If your compiler does not come with versions of xmsg and xalloc, the Rogue Wave classes RWxmsg and RWxalloc will emulate them for you.

Class xmsg carries a string that can be printed out at the catch site to give the user some idea of what went wrong. This string is formatted and internationalized as described in "Localizing Messages" in Chapter 16.

Error Handlers

Tools.h++ uses the macro RWTHROW to throw an exception. If your compiler supports exceptions, this macro resolves by calling a function which throws the exception. If your compiler does not support exceptions, the macro resolves to call an error handler with prototype:

void errHandler(const RWxmsg&);

The default error handler aborts the program. You can change the default handler with the function:

typedef void (*rwErrHandler)(const RWxmsg&);
rwErrHandler rwSetErrHandler(rwErrHandler);

The next example demonstrates how a user-defined error handler works in a compiler that doesn't support exceptions:

#include <rw/rwerr.h>
#include <rw/coreerr.h>
#include <iostream.h>
 
#ifdef RW_NO_EXCEPTIONS
 
void myOwnErrorHandler(const RWxmsg& error){
  cout << "myOwnErrorHandler(" << error.why() << ")" << endl;
}
 
int main(){
  rwSetErrHandler(myOwnErrorHandler);  // Comment out this line 
                              // to get the default error handler.
  RWTHROW( RWExternalErr(RWMessage( RWCORE_GENERIC, 12345, "Howdy!") ));
  cout << "Done." << endl;
  return 0;
}
 
#else  //RW_NO_EXCEPTIONS
 
#error This example only for compilers without exception handling
 
#endif

The Debug Version of Tools.h++

You can build the Tools.h++ library in a debug mode, and gain a very powerful tool for uncovering and correcting internal errors in your code.

To build a debug version of the library, you must compile the entire library with the preprocessor flag RWDEBUG defined. You must compile the entire library and application with a single setting of the flag—either defined or not defined. The resultant library and program will be slightly larger and slightly slower. See the appropriate makefile for additional directions.

The flag RWDEBUG activates a set of PRECONDITION and POSTCONDITION clauses at the beginning and end of critical functions. These pre- and postconditions are implemented with asserts. A failure will cause the program to halt after first printing out the offending condition, along with the file and line number where it occurred.

RWPRECONDITION and RWPOSTCONDITION

In his landmark book Object-oriented Software Construction, Bertrand Meyer suggests regarding functions as a contract between a caller and a callee. If the caller agrees to abide by a set of preconditions, the callee guarantees to return results that satisfy a set of postconditions. The following comparison shows the usefulness of Meyer's paradigm in Tools.h++. Let's look first at a bounds error in C:

char buff[20];
char j = buff[20];                      // Bounds error!

Such a bounds error is extremely tough to detect in C, but easy in C++, as shown below:

RWCString buff(20);
char j = buff[20];                     // Detectable bounds error

The bounds error is easy to detect in C++ because the operator[] can be overloaded to perform an explicit bounds check. In other words, when the flag RWDEBUG is set, operator[] also executes the PRECONDITION clause, as below:

char& RWCString::operator[](size_t I){
   RWPRECONDITION(i < length() );
   return rep[i];
}

The case just described would trigger a failure because operator[]would find that the PRECONDITION is not met.

Here's a slightly more complicated example:

template <class T> void List::insert(T* obj){
   RWPRECONDITION( obj!= 0 );
   head = new Link(head, obj);
   RWPOSTCONDITION( this->contains(obj) );
}

In this example, the job of the function insert() is to insert the object pointed to by the argument into a linked list of pointers to objects of type T. The only precondition for the function to work is that the pointer obj not be null. If this condition is satisfied, then the function guarantees to successfully insert the object. The condition is checked by the postcondition clause.

The macros RWPRECONDITION and RWPOSTCONDITION are defined in <rw/defs.h> and compile out to no-ops, so long as the preprocessor macro RWDEBUG is not defined. Here's what appears in the makefile:

#ifdef RWDEBUG
#  define RWPRECONDITION(a)      assert(a)
#  define RWPOSTCONDITION(a)     assert(a)
#else
#  define RWPRECONDITION(a)      ((void*)0)
#  define RWPOSTCONDITION(a)     ((void*)0)
#endif