[comp.lang.misc] Programming for errors

kers@hplb.hpl.hp.com (Chris Dollin) (11/24/90)

There's been a lot of discussion recently about coping with erros. A friend of
mine has composed this, on which he'd like comments; post them or mail to me,
please.

Regards, Kers.

;;; --------------------------------------------------------------------------

PROGRAMMING FOR ERRORS

Introduction

Any programmer comes to terms with errors early in their career. Most people
think of errors as things to be eradicated from programs, compilation errors,
bugs and so on. This paper is not about that at all. Instead, I want to
discuss the pitfalls involved in writing programs, and libraries of modules
which behave 'well' even when faced with errors outside programmer control.
For example, anyone can write a program which reads in two numbers and prints
their sum. In C, for example, such a program is only a few lines long.
However, try to write a program which does the same thing, but if faced with
only one number, or invalid characters in the data or something, manages to
respond with a helpful message, and even this simple program gets messy.

Things become even worse when a program has to handle not only user mistakes,
but also all the rare but possible things which can go wrong with the
computer. How many programs have you seen which recover gracefully if the disk
you are writing to is full, or too many files are open at once within the
system, or ( in a multi-user system ) someone else has interfered with your
data.

My experience has been that writing in a language like C, a program which
takes care of all such errors in a reasonable way is harder to write, harder
to debug and harder to maintain than one which just ignores the possibility.
Unfortunately, the program which ignores the possibility of these errors may
end up corrupting data or losing work or just frustrating the user if one of
those nasty things does happen.

The rest of this paper will try to identify the various different things which
are called errors, and to devise a reasonable way of dealing with them. This
ultimately should, I believe, involve a number of changes to the definition of
common programming languages to cope, but a lot can be done already. Later, my
own implementation of a reasonable compromise in C++ will be described. The
examples, and the experience on which they are based, are founded in the
Unix/C world, but many aspects are equally relevant to other languages.


A simple example

It is always easiest to understand a problem by seeing an example. Here is
what should be a simple program, and what happens to it when we start to worry
about errors.

#include <stdio.h>

main()
{
int i;

while(1)
 {
 scanf("%d",&i);
  if (i == -1) break;
 printf("The square is %d\n",i*i);
 }
}

This program, of course, prompts for a number. If it gets -1 it exits,
otherwise it prints the square and goes round again. Given the following input

1 4 100
3 6
-1
it produces
The square is 1
The square is 16
The square is 10000
The square is 9
The square is 36

as expected.

Unfortunately, the program is not at all robust. The first problem arises if
we give a non-number. You might expect the program to treat it as 0, or
possibly to exit with an error report. Instead, it just loops forever. Thus:

3 hello
as input gives
The square is 9
The square is 9
The square is 9
The square is 9
The square is 9
The square is 9
The square is 9
...
and so on.

The second problem occurs if the number we specify is too large for its square
to be a valid integer. If you try 1000000 as input you get -727379968 or
similar nonsense on a typical 32 bit machine.

The third problem occurs if some operating system error happens. We might try
to send output to a file not a terminal using the Unix > redirection, and the
file could be on a disk slice which is full up. In that case, the output will
be lost, but the program will continue anyway.

In order to handle these errors, we need firstly to decide what to do in such
cases. For example, it is probably quite reasonable simply to exit in the
third case, perhaps with a warning message to stderr. ( If stderr is also
damaged, we give up totally ). In the first case, we should print out a
different message, say

 - invalid character found. while in the second case we need to say
 - is not within range.

The modified program could look like this:

#include <stdio.h>

main()
{
int i,i2;
int result;
int ch;
while(1)
 {
 result = scanf("%d",&i);
 if (result <= 0)
  {
  ch = getchar();
  if (ch == EOF) break; /* We got an end of file */
  result = printf("%c -invalid character found\n",ch);
    if (result < 0) goto abort;
  continue;
  }
  i2 = i * i;
  if ((  i2 / i ) != i)
  {
  result = printf("%d - is not within range\n",i);
   if (result < 0) goto abort;
  }
  if (i == -1) break;
 result = printf("The square is %d\n",i*i);
  if (result < 0) goto abort;
 }
return(0);
abort:
  fprintf(stderr,"Fatal error writing to stdout - aborting\n");
  return(1);
}

The points to note are:
  - notice the use of a goto to handle a fatal error. This is a theme we will
expand on below.
  - the modified program is much larger than the original, less easy to follow,
and contains at least one nasty trick ( the division to see if the square was
out of range )


The cleaning up problem and exceptions

The example we have looked at was particularly simple, because there was no
concern about what to do to exit a routine cleanly. Quite a lot of routines,
especially those neither at top level or bottom level, may need to do some
kind of cleaning up before they exit. This issue affects error handling
because a very common reason to want to leave a routine without going all the
way through is as a result of an error. However, it applies equally to
routines which may do some setting up, processing, and clearing up afterwards.

As an example, suppose we have a program which reads records from a file, and
returns the first record which meets certain conditions. We want to guarantee
that the file is closed before the routine exits.

Our first attempt might look like this:

doit()
{
int fd = open_the_file;
if file not opened return error;
  while(1)
  {
  get a record from fd;
  if (end of file)
    {
    close the file;
    return failure;
    }
  if (some other error)
   {
   close the file
   return the error
   }
  check the record;
  if found
    {
    close the file;
    return success;
    }
 }
}

For the moment, we won't worry about how we distinguish success, failure or an
error ( we might return 0 for success, -1 for failure, and 1 for an error, in
which case we set up a global error variable with the error type ). The
problem with the above solution is that the clearing up code occurs three
times, and although it is fairly simple, it is still pretty bad practice to
repeat the same stuff in different places. A better solution would be:

clearup()
{
close the file;
}

doit()
{
int fd = open_the_file;
if file not opened return error;
  while(1)
  {
  get a record from fd;
  if (end of file)
    {
    clearup;
    return failure;
    }
  if (some other error)
   {
   clearup;
   return the error
   }
  check the record;
  if found
    {
    clearup;
    return success;
    }
 }
}

Unfortunately, that requires clearup to have access to the local fd which is
not in its scope. Some languages, eg pascal, would allow this, by making
clearup a routine defined within the scope of doit, but C does not. We could
make fd global, or pass it as a parameter, but such ideas get out of hand if
the clearup process is more complicated - precisely the circumstance we are
most concerned about. So, consider the following further possibility:


doit()
{
return_value result;
int fd = open_the_file;

if file not opened return error;
  while(1)
  {
  get a record from fd;
  if (end of file)
    {
    }
  if (some other error)
   {
    result = the error;
    goto clearup;
   }
  check the record;
  if found
    {
    result = success
    goto clearup;
    }
 }
clearup:
  close the file
  return result

}

This solution looks bad to those who have learned to treat goto's with
suspicion, but has in fact a lot going for it. Firstly, the clearup code is
guaranteed to be obeyed, regardless of how the routine is exited. We simply
replace the usual return(result) code with result = value;goto clearup;
something we could even hide in a C macro. Secondly, the clearup code is right
there in the function, not hidden away elsewhere. Thirdly, the code
automatically has access to all of the variables of the function, so it could,
for example, conditionally close files depending on whether they had in fact
been opened, or whatever.

This particular style of programming has led to a number of languages,
including ADA and GNU C++ providing support for a so-called exception
construct. This is essentially the same as a goto as above, with the following
differences:

  a) An exception must be structured with proper scope, so that you can have
nested exceptions in a way which enforces the control flow.
  b) One can pass exceptions as parameters to routines, to allow them to
call the exception directly rather than having to put the goto in at the
higher level each time.

However, exceptions don't solve all our problems. We still need that annoying
extra result variable to allow us to decide first what to return, and then
raise the exception. Furthermore, if a routine uses exception results, every
routine which calls it needs to know. It is also very difficult to arrange for
exceptions to essentially do nothing, and return control to the main program
anyway. Finally, exceptions don't actually address the issue of what precisely
went wrong at a lower level.

As a result, while exceptions are a useful alternative to gotos for clearup
type work, they don't deal well with many kinds of error. To address that
problem, we need to look at errors more closely.

Errors, a closer look.

It is time now to try to categorise the different types of error which can
occur.

1) Precondition violation.

As I explained in the introduction, this paper is not about programming errors
or bugs as such. However, if I am writing a module of routines for use by
separate programs, I can't guarantee that the calls to my modules will always
pass the correct information. Some languages like C++ or Pascal will spot some
basic errors ( wrong number or types of parameters ), while pre-ansi C will
fail even to do that much at compile time.  In general, it is impossible to
guarantee that a module will be robust and behave sensibly regardless of such
things as passing variables instead of pointers, missing out parameters,
getting them in the wrong order, and all the other little difficulties beloved
of C programmers. That problem is expressed by saying that the 'preconditions'
for the module/routine are not met, which, in other words means that the
behaviour of the module is undefined. If you are lucky, you may get a
diagnostic dump with some clues as to what you did wrong. If not, the program
may do something totally unexpected later on as a result of a 'scribble'.


2) 'Expected' incorrect input.

A very  common situation occurs when a routine is written to handle data which
will have come directly and not yet checked from a user. Such routines must
typically consider a large number of possible ways in which the input may fail
to meet expectations, and what to do. This problem is epitomised by that of
writing a compiler, where the quality of the compiler depends at least as much
on how good the error messages are as on how good the code generated for an
error free program may be. At a simpler level, our first example demonstrates
this case also. The original program treated any invalid input as a
precondition violation, in that it paid no attention to what would happen in
that case. The second one widened its scope to treat some possible mistakes as
'expected' and deal with them better.

In general, a well-written routine will treat as much as possible as
'expected' and always attempt to do something reasonable. Only where the
language makes it impossible to do better should unexpected behaviour occur.
That still begs the question of what 'reasonable' means. As we shall see, most
languages make it rather hard to be reasonable all the time.


3) Failures during processing.

The next case occurs when a routine has been given acceptable data, but at
some point during its processing, cannot perform some task. This may be
because the input data turns out not really to be acceptable after all, or
because some system resource was unavailable or refused, or even because an
internal code check showed up a bug in the routine's handling of the current
data. In any event, the routine needs to identify the cause, and have some way
of reporting what happened.

There is not much one can do about precondition violations except to try
wherever possible to move them into one of the other categories by widening
the scope of the routine. On the other hand, expected errors will have an
expected response, and the caller of the routine will know to check that
response and behave accordingly.  The rest of the paper will concentrate on
the third case. From now on error will mean only error in that sense.

The normal possibilities for error handling.

Error handling presents a challenge at three different levels. First, there is
the bottom level, i.e. the routine  which first detects the error. This is
commonly an operating system routine such as read or write, but it may be a
user-written routine also.

Secondly, there is the top level. This is either the main program, or possibly
a routine called from the main program but specific to it.

Finally, and as we shall see, hardest of all, there are intermediate routines.
These routines may themselves detect errors and have to report them, but they
may also call lower level routines which lead to errors, and have to be dealt
with too.

Let's look at the top level program first. At this level, we have a number of
key bits of information which are not available further down. Firstly, we will
know whether a detected error is serious and should result in immediate
program termination, or trivial and safe to ignore, or requires evasive action
of a non-fatal sort. Secondly, we will know how to report any errors back to
the user. In some cases this may be via a screen-based window, in others to a
system log. The point is that only the top level program can really decide
because only it is guaranteed to know how and on what it is running.

At the other extreme, the bottom level routine knows exactly what went wrong,
but not necessarily what to do about it. Most languages give these
alternatives:

  - ignore the error
  - send a report to an error device
  - crash the program with an error report
  - call a user-defined routine
  - return with a special value set to indicate the error

or some combination of the above. Of these, we can immediately reject all but
the last two. The other three pre-empt the decision about how to handle the
error, rather than allowing that decision to be made higher up. So, let's
consider the final two.

  - call a user-definedz routine

allows the user at any time to specify a specific routine which will be called
whenever an error ( or possibly only certain kinds of error) occurs. This
routine may do any of the first three possible actions, or it may do something
completely different to recover from the problem. At first sight, this
approach, which corresponds to the Unix/C signal system used for so-called
fatal errors, might appear to be the perfect solution.  However, when we come
to consider intermediate routines, it becomes clear that there are a number of
problems. In the meantime, we can usefully list the advantages in those cases
where this approach does work.

Firstly, the hander routine can be set up by main, and so can be written in
the full knowledge of the environment in which the program is operating.

Secondly, once main has set up a handler, it can often then simply ignore the
possibility of an error in its main code. This results in much cleaner and
easier to read code. It also reduces the possibility that some routine may
fail without the caller noticing.

Thirdly, it is usually possible to arrange for a handler routine to perform
either directly or indirectly via a goto any clean up of their system which
may be needed before exiting. Thus, we can gain the benefits of the exception
handling style described above.

Let's compare that with the final possibility - simply flagging the error,
either via a return or write-back parameter, or by using a global variable.
This is the method favoured by C for most errors in the operating system
interface. Usually a global integer errno is set to indicate the nature of an
error, whose presence is indicated by passing an 'impossible' result back to
the calling routine. This approach has the following advantages.

Firstly, whenever things do go wrong, the calling routine has every
opportunity to identify that fact, and even identify the precise nature of the
problem so as to decide what to do next.

Secondly, it is easy for an intermediate routine to decide that a lower level
error is not serious, and cancel it altogether, or to replace it with a
different more relevant error

Thirdly, there is an extremely low overhead when no error does in fact occur.

Fourthly, it is very easy to deal with errors in different places differently.

Fifthly, if an exception/clearup is called for, this is easy to cause.

There are, however, two disadvantages which matter.  Firstly, it is up to the
programmer to test explicitly for an error at every point. To forget to do so,
or assume that one will not occur, may easily result in a major problem
elsewhere in the program, a bug which is non-repeatable and hard to track.

Secondly, code which does explicitly check for all possible errors ends up
looking awful. A small sample gives the idea:

Without error checking
.....
fd = open(filename,O_RDONLY);
write(fd,message,mlen);
lseek(fd,position,0);
read(fd,message2,mlen);
....

With error checking
fd = open(filename,O_RDONLY);
  if (fd < 0)
  {
  perror("open file");
  exit(1);
  }
i = write(fd,message,mlen);
  if (nread < 0)
  {
  perrror("write message");
  exit(1);
  }
i = lseek(fd,position,0);
  if (i < 0)
  {
  perror("lseek");
  exit(1);
  }
i = read(fd,message,mlen);
  if (i < 0)
  {
  perror("read");
  exit(1);
  }
  if (i < mlen)
  {
  printf("Only %d bytes available\n",i);
  exit(1);
  }
.......

Which is easier to follow? Of course, one could make things a bit easier by
writing a general purpose message and exit routine, but that would only save a
few lines, and in that case anyway, one would prefer to have used the
user-defined-routine method.

Things become even more complex when we consider the proper writing of
intermediate level routines. In the user-defined routine case, we might want
to handle certain errors ourselves, but a higher level may have set up a
user-defined routine. So, assuming we can, we need to stack the higher level
routine, set up our own, and then, depending on the error, promulgate it up by
calling the stacked routine, or clear it down. We must also ensure that we
restore the higher level routine on exit, which forces us to have a clearup
function even if none were otherwise needed.

Even in the error return case there is an additional problem. The system
defined error numbers may not include a value which describes the real cause
of the problem as far as the higher levels are concerned. Either the routine
wishes to return an error which relates to some internal factor, so the system
has not been involved at all, or just the system error is not really enough
information about the problem, whereas a dump and exit is too extreme.  We
need to be able to report not just the original error if any, but also
additional values reflecting the particular routine. Furthermore, any error
numbers should ideally be specific to the routine and not general.

This obsession with supplying information has a further pitfall. By the time
we return to top level, an error may have been passed up by several levels of
intermediate routine. If we end up with several different error values, how is
the program to make sense of them in a way which allows it selectively to
ignore some, report others, and fail on the rest. In order to ease this
problem, it is useful to identify various error classes, which describe what
sort of thing happened, without trying to be too specific as to details. If
these classes can be chosen well, it should only be necessary to examine the
class of the original error to decide how to act, and a more detailed
examination of the error numbers becomes exceptional.

So, where have we got to?

Currently, we have identified two reasonable ways of dealing with error
information where different routines are involved, user-defined functions and
error number(s). We have identified the exception ( possibly implemented as a
controlled goto ) as a sensible way of ensuring routine clearup. We have seen
that these techniques, even in combination, have a number of undesirable
features. In the next section, I discuss a few reasonable additions to
languages which might help to solve the problem.


The idealised solution requires language support

In the section on exception handlers, we mentioned that some languages,
including incidentally many versions of FORTRAN, provide a method of passing
exceptions or labels as parameters to routines. This allows the routine, if it
fails, to cause an exception at the level above, in addition to setting an
error number or whatever. FORTRAN goes one better, by making the routines
treat the appropriate class of error as fatal if the exception label is
omitted. What it doesn't do is allow the user to write such code in their own
routines.

What we really want is a slight extension of this idea. When calling a
routine, one should be able to do so in one of three ways, each syntactically
easy to code.

  Firstly, one should be able to specify a global action whenever an unhandled
error occurs. This action should automatically stack with the calling routine,
so that it can be overridden at a lower level without affecting higher levels.

  Secondly, one should be able to specify a label or exception to go to if the
routine fails.

  Thirdly, one can require that the routine on failure should return control
as normal.

  Fourthly, there should be built-in support for specifying the return value
of the routine without yet returning, and for setting up a return value, and
going to the clearup code.

  In all cases, a stack of error numbers which give a list of errors from the
bottom level up should be availabe to examine, along with an error class for
control flow decision making. Whenever a routine fails, the caller must stack
a code of its own to indicate the routine through which the error was passed.
It may, alternatively, replace the entire stack with a new one of a different
class or description if appropriate.

Errors in error handlers.

At this point, you might think that we have just about exhausted the issue.
There is, however, one remaining rather nasty issue.
What happens if, while clearing up as a result of an error, one of our
clearup routines itself fails. As always there are several possibilities:
  - Ignore the new error completely, and ensure that such failure have no
    untoward results.
  - Treat the new error as unconditionally fatal
  - Be able to supply a special emergency error handler for failures during
    clearup ( etc etc ? )
We can reject the second solution as we rejected unconditional fatal errors
before.

The third solution is workable, and the most general, but requires the ability
to save the original error stack and use a new one for handling the current
level of clearup, and so on and so on.

In practice, the first solution turns out to be the best under normal
circumstances. Typically, clearup routines only fail if the data they were
handling is now useless, so failing won't make anything worse than it was. On
the other hand, later clearup functions may be successful, and it would be a
shame to lose the chance to run them.

An implementation in C which works in practice

Now that we've decided more or less what we want, let's look at a series of
routines and macros in C which comes some way towards achieving our goal.

Consider the following code

struct errorinfo
{
jmp_buf b;
struct errorinfo *prev;
};

int globalerror;
int clearing_up;

struct errorinfo *errorlist;

resulttype myproc()
{
resulttype result;
struct errorinfo *ei = malloc(sizeof(struct errorinfo));
ei->prev = errorlist;
errorlist = ei;

  if (setjmp(ei->b) != 0) goto finish;

.... here goes the main code ...

result = (the final result);

finish:
clearing_up ++;
... a whole load of clear up routines
clearing_up --;
errorlist = ei->prev;
free(ei);
  if (globalerror) longjmp(errorlist->b);
return result;
}

If left to its own devices, this routine will simply stack a longjump address
so that anything calling the appropriate longjmp will go straight to the
finish label in that routine.

Suppose that within the code somewhere we have the line:

  globalerror = x;goto finish;

This will have the effect of a controlled jump on a failure event.
Furthermore, the routine will exit with a jump to the finish label of its
calling routine. This scheme has the tremendous advantage that it stacks
properly. In other words, however deep you are, setting globalerror and
exiting will take you all the way back to top level, with the finish routines
called each time. The purpose of clearing_up is to ensure that if it is
non_zero, the routine can tell that an error is already being handled, and
treat further failure appropriately.

The major disadvantage of the above scheme is that it relies on all routines
behaving properly. The consequences of doing a direct return without clearup
are rather nasty in terms of future behaviour on errors. However C++ gives us
the chance to avoid these problems. If we put the handling of errorlist into
constructors and destructors for an error class, we can ensure that the
destructor is called however we exit. At the same time we can allow a number
of other improvements to the simple scheme above. We can maintain a stack of
errors as well as an error class, which allows us to identify precisely where
an error occurs. We can use macros to hide the workings from the user. We can
also allow a flag at each level to disable the automatic jump, allowing us to
intercept lower level errors and reset or alter them. As a result, we end up
with the following implementation.

///////////////////// errors.h ///////////////////////////////

#define MAXNUERRORS 10 #define MAXNAMELEN  30

extern int n_u_errors;
extern int clearing_up;
extern int32 u_error[MAXNUERRORS];
extern int32 errorclass;
extern char u_name[MAXNUERRORS][MAXNAMELEN+1];

struct ui_error_handler
{
public:
void onerror(int n= -1);
void pusherror(int32);
void reseterror();
void report_system_error(char *name);
ui_error_handler(char *c);
~ui_error_handler();

int next_error_return_val;
jmp_buf jumpto;

private:
int explicit_error;
char *name;
ui_error_handler *prev;
int32 mask;
};
#define EC_TEMPRES   1
#define EC_INVALID   2
#define EC_PERM      3
#define EC_STATE     4
#define EC_CORRUPT   5
#define EC_INTERRUPT 6
#define EC_HARDWARE  7
#define EC_INTERNAL  8
#define STARTDEFS(name) ui_error_handler eh(name)
#define ENDDEFS {if (setjmp(eh.jumpto)) goto finish;}
#define STARTCLEARUP finish: clearing_up++
#define ENDCLEARUP (clearing_up--)

#define WASERROR  ((n_u_errors > 0) && (! clearing_up))
#define HADERROR  (WASERROR && (eh.reseterror(),1))
#define PUSHERROR(errnum) { if (!clearing_up) eh.pusherror(errnum);}
#define IFFAIL(errnum,i) {if WASERROR {eh.pusherror(errnum);result = (i);goto finish;}}
#define FAIL(i) {if WASERROR {result = (i);goto finish;}}
#define NEXTERROR(i) ( eh.next_error_return_val = i )
#define CLEARUP {goto finish;}
#define RETURNERROR(class,errnum,s,i) { if (!clearing_up) {eh.reseterror();eh.pusherror(errnum);errorclass = class;result = (i);goto finish;} }
#define NORETURNERROR(class,errnum,s) { if (!clearing_up) {eh.reseterror();eh.pusherror(errnum);errorclass = class;} }
#define RESETERROR eh.reseterror()
#define ONERROR(n) eh.onerror(n)
#define SYSTEMERROR(name) eh.report_system_error(name)

/////////////////////// errrors.cc ////////////////////////////
#include < ... various operating system files ... >
#include <errors.h>

extern int errno;
int n_u_errors;
int clearing_up;
int next_error_return_val;
int32 errorclass;
int32 u_error[MAXNUERRORS];
char u_name[MAXNUERRORS][MAXNAMELEN+1];
jmp_buf global_jump_buf_list[20];
int global_jump_buf_level;
int32 global_jump_buf_mask[20];
int (*default_error_proc)();
static ui_error_handler *top_errorhandler = 0;

ui_error_handler::ui_error_handler(char *c)
{
    mask = 0;
    if (n_u_errors && (clearing_up == 0))
    {
        longjmp(top_errorhandler->jumpto,1);
    }
    name = c;
    prev = top_errorhandler;
    top_errorhandler = this;
    next_error_return_val = 2000;
    explicit_error = 0;
    if (clearing_up == 0) n_u_errors = 0;
}
ui_error_handler::~ui_error_handler()
{
    top_errorhandler = prev;
    if (prev == 0) return;
    if ( (clearing_up==0) && n_u_errors) 
    {
        if(!explicit_error)
        {
            char *dest,*n;
            n_u_errors++;
            u_error[n_u_errors-1] = next_error_return_val;
            dest = u_name[n_u_errors-1];
            n = name;
            while(*n) 
            *(dest++) = *(n++);
            *dest = 0;
        }
        if (prev->mask) longjmp(prev->jumpto,1);
    }
};

void ui_error_handler::onerror(int n)
{
    mask = n;
};


void ui_error_handler::reseterror()
{
    n_u_errors = 0;
    clearing_up = 0;
}

void ui_error_handler::pusherror(int32 errnum)
{
    char *dest,*n;
    explicit_error = 1;
    n_u_errors++;
    u_error[n_u_errors-1] = errnum;
    dest = u_name[n_u_errors-1];
    n = name;
    while(*n) *(dest++) = *(n++);
    *dest = 0;
}

char *description_corresponding_to_errorclass(int errorclass)
{
    switch (errorclass)
    {
        case EC_TEMPRES:
        return "Temp resource problem";
        case EC_INVALID:
        return "Invalid request";
        case EC_PERM:   
        return "Permission problem";
        case EC_STATE:
        return "Resource in inappropriate state";
        case EC_CORRUPT:
        return "Resource corrupted";
        case EC_INTERRUPT:
        return "Call interrupted";
        case EC_HARDWARE:
        return "Hardware/external problem";
        case EC_INTERNAL:
        return "Internal error or bug";
        default:
        return "Unknown problem";
    }
}
class report_error_on_exit
{
    public:
    report_error_on_exit() {};
    ~report_error_on_exit();
};
report_error_on_exit::~report_error_on_exit()
    {
        int i;
        if (n_u_errors)
        {

        cerr << "\n ** main program exited with the following errors **\n" <<
            " ** with error class: " <<
            description_corresponding_to_errorclass(errorclass) << " **\n";
        for(int i = 0; i < n_u_errors; i++)
            cerr << u_error[i] << " in " << u_name[i] << "\n";
        cerr << "\n";
                }
    }

static class report_error_on_exit exreport;

void ui_error_handler::report_system_error(char *name)
{
    int ec = EC_INVALID;
    reseterror();    
    switch (errno)
    {
        case 1:
        case 13:
        case 30:
        case 27:
        ec=   EC_PERM;
        break;
        case 11:
        case 12:
        case 16:
        case 23:
        case 24:
        case 26:
        case 28:
        case 31:
        case 36:
        case 37:
        case 72:
        case 67:
        case 68:
        case 69:
        case 55:
        case 59:
        case 35:
        case 73:
        case 74:
        ec=EC_TEMPRES;
        break;
        case 9:
        case 56:
        case 57:
        case 58:
        ec=EC_STATE;
        break;
        case 32:
        case 75:
        case 70:
        case 53:
        ec = EC_CORRUPT;
        break;
        case 4:
        ec= EC_INTERRUPT;
        break;
        case 60:
        case 64:
        case 65:
        case 52:
        case 50:
        case 5:
        ec = EC_HARDWARE;
        break;
    }
    char *dest,*n;
    n_u_errors++;
    u_error[n_u_errors-1] = errno;
    dest = u_name[n_u_errors-1];
    n = name;
    while(*n) *(dest++) = *(n++);
    *dest = 0;
    errorclass = ec;
}

Conclusion

As we have seen, it is possible, although not nicely, to meet most of our
objectives within the existing framework of C++. The system above has been in
succesful day to day use on at least one significant project, and has proved
its worth. However, it is by no means a perfect solution, and the author would
be very pleased to receive suggestions for improvements, and also for
consistent modifications to the specifications of C++ or other languages to
build these features in.


HERE ENDS THE DOCUMENT
--

Regards, Kers 24059 | "You're better off  not dreaming of  the things to come;
Caravan:            | Dreams  are always ending  far too soon."