Data Structures and Algorithms
|
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 same
processor!
How can we expect it to be useful if we transport this program
to another system altogether, eg a Macintosh or a Windows
machine?
Before looking at what we can do in ANSI C, let's look at how some
other languages tackle this problem.
© John Morris, 1998