6.6 Handling Errors in Library Modules

There are a number of common methods of detecting and handling errors that may take place in some specialized section of the program, particularly while some library module code has control of the program execution. Some of the simpler ones are:

Precondition checking

This method relies on checking for potential error conditions before taking the action which might otherwise cause the error to arise. Code to compute the tangent of an angle by the formula tan (x) = sin (x) / cos (x) could possibly look something like this outline:

  IF (abs ( cos (x)) > 0.000001)  (* or a suitable small number *)
    THEN
      m := sin (x) / cos (x)
    ELSE
      take evasive error action
    END;

The module Fractions in the last section relies on precondition checking to prevent errors, for it demands that parameters be free of conditions that would result in a zero denominator. It places the responsibility for doing the checking on the client program. Notice however that none of the procedures will actually fail to work if the preconditions are not met. There are no divisions performed in the implementation, so a fraction of (1, 0) for instance will not generate a divide-by-zero error at run time. However, to maintain the integrity of the abstract representation, zero denominators ought to be prohibited, and precondition checking is one way to require this.

Postcondition Checking

This is the method employed by such modules as STextIO, SRealIO, and SWholeIO that allow for the function SIOResult.ReadResult to enquire about the success of the last performed read operation. Similarly, the classical InOut maintains a global variable Done that is set by a certain class of operations automatically. In this method, the operation is attempted, and its success is checked afterward (perhaps for several tries) by examining the value of the appropriate variable.

  REPEAT
    WriteString ("Please type in a real number ===> ");
    ReadReal (numToGet);
    tempResult := ReadResult ();
    SkipLine; (* swallow line marker *)
    WriteLn;
  UNTIL tempResult = allRight;

or, perhaps:

  try := 1;
  REPEAT
    WriteString ("Please type in a real number ===> ");
    ReadReal (numToGet);
    tempResult := ReadResult ();
    SkipLine; (* swallow line marker *)
    WriteLn;
    IF tempResult # allRight
      THEN
        WriteString ("Error in input. Try again");
        WriteLn;
        INC (try);
      END;
  UNTIL (tempResult = allRight) OR (try = 5);
  (* handle more than 5 bad tries here somewhere *)

Alternately, the same thing could be achieved, but with more pressure on the programmer to actually check the result if a "success" variable is included as one of the parameters in the procedure. Since an actual parameter must be provided at each call, the variable cannot be simply ignored.

REPEAT
  WriteString ("Type in a Real here = = >");
  ReadReal (theReal, allOK); (* this ReadReal has two parameters *)
UNTIL allOK;

This type of error handling (in either style) can easily be added to library modules created by the programmer. The first can be achieved using an enumerated type with or without an enquiry function, or a boolean. The first value in the enumeration represents a no-error condition, and the others represent specific problems that have been encountered. For instance, the definition part of the module Fractions could have:

  TYPE
    FracStatus = (fracOk, undefined, divideByZero);

  PROCEDURE FracState (): FracStatus;

The implementation module maintains a variable, say, fracState to store the last error state. With these in place, the procedure Assign would be written to set fracState to the value fracOk whenever the denominator being set was non zero, and to undefined if it were zero. The procedure Div would set fracState to divideByZero if the second parameter it were passed had a zero numerator, and to fracOk otherwise. The other procedures would not give rise to such errors and could all set fracState to fracOk. (An overflow in the integer type during Add or Mul could also lead to an error, but that would be more difficult to handle in this manner). If a client of this module wanted to determine the value of this variable after an operation it would use the procedure FracState to do so.

Automatic Error Handling

Methods that take this approach invoke some procedure that automatically handles errors whenever they take place, without any boolean or enumerated type having to be set or checked. A detailed discussion of such methods cannot be included here, but will be taken up later in the text. One uses procedure variables, and will be mentioned as an application of these once they are introduced. The other makes use of exceptions, and is only available to the programmer if some changes have been made to the Modula-2 notation itself. Such changes have been incorporated into the ISO standard for the notation, but are not available in classical Modula-2 as defined by Wirth. Exception handling will be covered in chapter 10.

Which of the methods is best? Some would respond that this is merely a matter of taste and style, but others would say that the question is a fundamental one that goes to the heart of proper program design philosophy. This text takes the position that precondition checking is generally preferred, on the assumption that errors are better caught before they can happen, but that postcondition checking and exception handling are sometimes necessary. The most important thing is that all errors be caught and handled in some way, and that whatever method is used be clearly documented in the definition and implementation parts of library modules and in client programs.


Contents