10.9 Error Handling Revisited

Careful planning, accurate coding in a structured notation, and diligent care in debugging can greatly increase confidence in the reliability and correctness of a piece of code. It must be understood, however, that testing is undertaken to find errors. No amount of testing proves that software is error free.

There is always one more bug. --a Corollary of Murphy's Law

The first thing that any engineer must learn is that Murphy's Law is not a joke. The good engineer knows there will be failures, and takes that into account. Specifically, it is not enough to plan what is thought to be error free software and test it thoroughly after it has been produced. Provision must still be made to handle the errors the programmer thinks cannot occur when they do in fact occur. No compiler can alert the programmer to logical errors; the ability to do so would mean that all possible logically correct programs were explicitly or implicitly known to the compiler, which is not possible. Indeed, it can be shown mathematically that it is not possible to build a program that can examine all programs to determine if they will be successful in their tasks or not.

Thus, one is always required to develop a variety of error handling techniques. Of course, the best kinds of errors are those that are prevented from happening in the first place. That is why section 6.6 emphasized precondition checking as a means of reducing errors. However, even if the software requiring certain preconditions is itself correct, and those preconditions are carefully met by the calling code, there may still be errors of two kinds:

1. errors caused by the failure of external library module routines, such as those attempting to read information from a file
2. unforeseen errors in either the client code or the pre-conditional code

The first kind is outside the programmer's direct control, and as for the second, there are always conditions the programmer has not thought of, and so there are possible errors unrelated to the checked conditions.

10.9.1 Typical Library Errors

For such reasons, library modules often export an error type so as to allow postcondition checking once the library routine has returned control to the client program. The simplest method is to export a boolean variable, say, done, which is set to TRUE whenever an invoked library routine succeeded, and to FALSE whenever it failed. This is the method employed by the classical libraries InOut and RealInOut.

There are two disadvantages to such a technique. The first is that a Boolean variable cannot convey information about why the failure took place. This problem can be solved by making the error type an enumeration, as in the ISO libraries, where one has:

  ReadResults = 
  ( notKnown,      (* no data read result is set *)
    allRight,      (* data is as expected or as required *)
    outOfRange,    (* data cannot be represented *)
    wrongFormat,   (* data not in expected format *)
    endOfLine,     (* end of line seen before expected data *)
    endOfInput     (* end of input seen before expected data *)
END IOConsts.

Likewise, the module ChanConsts exports (for the import and re-export of device drivers):

  OpenResults =        (* Possible results of open requests *)
    (opened,           (* the open succeeded as requested *)
     wrongNameFormat,  (* given name is in the wrong format for the implementation *)
     wrongFlags,       (* given flags include a value that does not apply to the device *)
     tooManyOpen,      (* this device cannot support any more open channels *)
     outOfChans,       (* no more channels can be allocated *)
     wrongPermissions, (* file or directory permissions do not allow request *)
     noRoomOnDevice,   (* storage limits on the device prevent the open *)
     noSuchFile,       (* a needed file does not exist *)
     fileExists,       (* a file of the given name already exists when a new one is required *)
     wrongFileType,    (* the file is of the wrong type to support the required operations *)
     noTextOperations, (* text operations have been requested, but are not supported *)
     noRawOperations,  (* raw operations have been requested, but are not supported *)
     noMixedOperations,(* text and raw operations have been requested, but they are not supported in combination *)
     alreadyOpen,      (* the source/destination is already open for operations not supported in combination with the requested operations *)
     otherProblem      (* open failed for some other reason *)

The second difficulty with using variables is that an error variable is not safe, for it can be changed by the client program. Programmers are fond of reusing such variables by attaching additional conditions to the read in the following manner:

Done := Done AND (num >= 10);

thus destroying the original value of a variable that does not belong to the module making the change. To prevent this, library modules should not export error variables (and should rarely, if ever, export variables at all). Rather, they should export only enquiry procedures that return a value of the error type as is done in the ISO I/O library module:

IMPORT IOConsts, IOChan;
  ReadResults = IOConsts.ReadResults;

PROCEDURE ReadResult (cid: IOChan.ChanId): ReadResults;

END IOResult.

Likewise, a module like StreamFile has:

IMPORT IOChan, ChanConsts;
  ChanId = IOChan.ChanId;
  FlagSet = ChanConsts.FlagSet;
  OpenResults = ChanConsts.OpenResults;
PROCEDURE Open (VAR cid: ChanId; name: ARRAY OF CHAR; flags: FlagSet; VAR res: OpenResults);

so that, in this case, rather than an inquiry procedure, the error result is returned in a variable parameter of the module's typical procedure activities.

When both these suggestions are followed, a substantial amount of information can be obtained about the reasons why a file open or a disk read failed, without compromising the value of the variables where this information is stored (because the client program cannot see that variable).