Get new post automatically.

Enter your email address:


2.7 Error Handling

No program or program fragment can be considered complete until appropriate error handling has been added. Unexpected program failures are a disaster - at the best, they cause frustration because the program user must repeat minutes or hours of work, but in life-critical applications, even the most trivial program error, if not processed correctly, has the potential to kill someone.
If an error is fatal, in the sense that a program cannot sensibly continue, then the program must be able to "die gracefully". This means that it must
  • inform its user(s) why it died, and
  • save as much of the program state as possible. 

 

2.7.1 Defining Errors

The first step in determining how to handle errors is to define precisely what is considered to be an error. Careful specification of each software component is part of this process. The pre-conditions of an ADT's methods will specify the states of a system (the input states) which a method is able to process. The post-conditions of each method should clearly specify the result of processing each acceptable input state. Thus, if we have a method:
int f( some_class a, int i )
/* PRE-CONDITION: i >= 0 */
/* POST-CONDITION:
    if ( i == 0 )
        return 0 and a is unaltered
    else
        return 1 and update a's i-th element by .... */ 
  • This specification tells us that i==0 is a meaningless input that f should flag by returning 0 but otherwise ignore.
  • f is expected to handle correctly all positive values of i.
  • The behaviour of f is not specified for negative values of i, ie it also tells us that
  • It is an error for a client to call f with a negative value of i.
Thus, a complete specification will specify
  • all the acceptable input states, and
  • the action of a method when presented with each acceptable input state.
By specifying the acceptable input states in pre-conditions, it will also divide responsibility for errors unambiguously.
  • The client is responsible for the pre-conditions: it is an error for the client to call the method with an unacceptable input state, and
  • The method is responsible for establishing the post-conditions and for reporting errors which occur in doing so.

 

2.7.2 Processing errors

Let's look at an error which must be handled by the constructor for any dynamically allocated object: the system may not be able to allocate enough memory for the object. A good way to create a disaster is to do this:
X ConsX( .... )
    {
    X x = malloc( sizeof(struct t_X) );
    if ( x == NULL ) {
        printf("Insuff mem\n"); exit( 1 );
        }
    else
       .....
    }
Not only is the error message so cryptic that it is likely to be little help in locating the cause of the error (the message should at least be "Insuff mem for X"!), but the program will simply exit, possibly leaving the system in some unstable, partially updated, state. This approach has other potential problems:
  • What if we've built this code into some elaborate GUI program with no provision for "standard output"? We may not even see the message as the program exits!
  • We may have used this code in a system, such as an embedded processor (a control computer), which has no way of processing an output stream of characters at all.
  • The use of exit assumes the presence of some higher level program, eg a Unix shell, which will capture and process the error code 1.

As a general rule, I/O is non-portable!
A function like printf will produce error messages on the 'terminal' window of your modern workstation, but if you are running a GUI program like Netscape, where will the messages go? So, the same function may not produce useful diagnostic output for two programs running in different environments on the sameeg a Macintosh or a Windows machine? processor! How can we expect it to be useful if we transport this program to another system altogether,
Before looking at what we can do in ANSI C, let's look at how some other languages tackle this problem.