[Ada Information Clearinghouse]

"Rationale for the Design of the
Ada® Programming Language"

[Ada '83 Rationale, HTML Version]

Copyright ©1986 owned by the United States Government. All rights reserved.
Direct inquiries to the Ada Information Clearinghouse at adainfo@sw-eng.falls-church.va.us.

CHAPTER 14: Exception Handling

14.3 Examples

Several examples that show typical uses of exception handling are discussed in this section.
In this section...

14.3.1 Matrix Inversion
14.3.2 Division
14.3.3 A File Example
14.3.4 A Package Example
14.3.5 Example of Last Wishes


14.3.1 Matrix Inversion

The first example is adapted from [BFH 76]. Each iteration of a loop is supposed to read a matrix, invert it, and print the result. If the matrix is singular, a message is to be printed and the program is to proceed with the next matrix.
procedure MAIN is
  procedure TREAT_MATRICES(N :  INTEGER) is
    SINGULAR :  exception;
    ...
    procedure INVERT(M :  in out MATRIX) is
    begin
      -- compute inverse of determinant
      -- note : this may implicitly raise NUMERIC_ERROR
      -- complete inversion of the matrix.
    exception
      when NUMERIC_ERROR =>  raise SINGULAR;
    end INVERT;

    procedure TREAT_ONE is
      M :  MATRIX;
    begin
      READ(M);
      INVERT(M);
      PRINT(M);
    exception
      when SINGULAR =>  PRINT("Matrix is singular");
    end TREAT_ONE;

  begin -- TREAT_MATRICES
    for COUNT in 1 .. N loop
      PRINT("ITERATION");  PRINT(COUNT);
      TREAT_ONE;
    end loop;
  end TREAT_MATRICES;

begin
  TREAT_MATRICES(20);
end MAIN;

As this example illustrates, the possible occurrence of NUMERIC_ERROR within INVERT is envisaged and consequently an appropriate handler has been provided. On the other hand, an occurrence of this exception within READ or PRINT would cause termination of MAIN, since no handler has been provided within MAIN.

In order to illustrate the dynamic behavior of this program, let us consider the stack situation (stacking downward) during a call to INVERT:
(1)  MAIN                  calling TREAT_MATRICES

     TREAT_MATRICES        calling TREAT_ONE

     TREAT_ONE             calling INVERT

     INVERT                executing normal statements of INVERT
If NUMERIC_ERROR occurs during the inversions, the corresponding handler will be executed:
(2)  MAIN                  calling TREAT_MATRICES

     TREAT_MATRICES        calling TREAT_ONE

     TREAT_ONE             calling INVERT

     INVERT                executing the handler for NUMERIC_ERROR
Note that during its execution, the handler has access to the local variables and parameters of INVERT. Here the only effect of this handler is to raise the exception SINGULAR. As a consequence, the activation of INVERT is deleted (any incompleted result in M is abandoned) and the exception SINGULAR is propagated within TREAT_ONE at the point of call of INVERT. The handler of TREAT_ONE for SINGULAR is then executed:
(3)  MAIN                  calling TREAT_MATRICES

     TREAT_MATRICES        calling TREAT_ONE

     TREAT_ONE             executing the handler for SINGULAR
In this case the execution of the handler terminates the execution of TREAT_ONE without propagating an exception in TREAT_MATRICES. This leads to the following stack configuration where another iteration of the loop statement can now be performed:
(4)  MAIN                  calling TREAT_MATRICES

     TREAT_MATRICES        executing the loop statement
The above example is characteristic of a family of problems in which a sequence of items are subjected to a given treatment. Should this treatment fail for one item of the sequence, it would be unreasonable to abort the entire sequence. Rather, the exception handling facility provides the ability to do a partial termination - that of the current item.


14.3.2 Division

Consider the following definition of the function DIVISION:
function DIVISION(A, B :  REAL) return REAL is
begin
  return A/B;
exception
  when NUMERIC_ERROR =>  return REAL'LAST;
end;

Should NUMERIC_ERROR occur during the computation of A/B, the execution of the handler will complete the execution of the function DIVISION. Any statement that is valid within the sequence of statements of DIVISION is also valid in the handler. In particular the handler may provide the return statement

    return REAL'LAST;

on behalf of the function.

This example illustrates the nature of handlers. They must be viewed as substitutes, ready to take charge of the operations in case of error.


14.3.3 A File Example

This example shows a case where exception handling is used to treat an event that is certain to happen: reaching the end of a file. Naturally this example could be formulated with an explicit check for each iteration. Assuming the file to be quite large, however, the body of the procedure TRANSFER may be efficiently represented as an (apparently) infinite loop, and the final actions of the procedure performed by the exception handler for END_ERROR.
with TEXT_IO;
use TEXT_IO;
procedure TRANSFER is
  INPUT   :  FILE_TYPE;
  OUTPUT  :  FILE_TYPE;
  C       :  CHARACTER;

begin
  OPEN(INPUT,  MODE =>  IN_FILE, NAME =>  "SOURCE");
  OPEN(OUTPUT, MODE =>  OUT_FILE,    NAME =>  "DESTINATION");
  loop
    GET(INPUT, C);
    PUT(OUTPUT, C);
  end loop;
exception
  when END_ERROR =>
    CLOSE(INPUT);
    CLOSE(OUTPUT);
end;

The procedure TRANSFER transfers the characters from the file SOURCE into the file DESTINATION. At each iteration GET is called, and eventually an END_ERROR exception will occur. Then the corresponding handler will be activated and its execution will complete the execution of TRANSFER.

This example shows that although many exceptions will represent error conditions, some of them may just be normal conditions for termination.


14.3.4 A Package Example

The example below reproduces a skeleton of the TABLE_MANAGER package that is described in the Reference Manual section 7.5.
package TABLE_MANAGER is
  type ITEM is ...
  ...
  procedure INSERT    (NEW_ITEM   :  in   ITEM);
  procedure RETRIEVE(FIRST_ITEM   :  out  ITEM);
  TABLE_FULL :  exception;      -- raised by INSERT when table full
end;

package body TABLE_MANAGER is
  ...
  procedure INSERT(NEW_ITEM :  in ITEM) is
  begin
    if FREE_LIST_EMPTY then
      raise TABLE_FULL;
    end if;
    -- remaining code for INSERT
  end;
  ...
end TABLE_MANAGER;

The interface of the table manager defines the operations INSERT and RETRIEVE, and the exception TABLE_FULL. Any procedure that uses the package may provide a local handler for this exception; for example:
procedure APPLICATION is
  use TABLE_MANAGER;
  ...
  procedure SAFE_INSERT(ELEMENT :  in ITEM) is
    NEXT :  ITEM;
  begin
    INSERT(ELEMENT);
  exception
    when TABLE_FULL =>
      RETRIEVE(NEXT);
      -- perform usual treatment of NEXT
      INSERT(ELEMENT);
  end SAFE_INSERT;
begin
  -- includes calls of SAFE_INSERT instead of INSERT
end APPLICATION;

Within procedure APPLICATION, a procedure SAFE_INSERT with a local handler for TABLE_FULL is provided. Should this exception be raised by the body of INSERT, the local handler for TABLE_FULL gains control and calls RETRIEVE before reiterating the call of INSERT. Should an exception occur again in this second call, the execution of SAFE_INSERT will be abandoned and the exception will be propagated to the caller of SAFE_INSERT.

It is worth mentioning that the body of INSERT is assumed to be programmed in a robust manner: it does not modify any global variable if it cannot accomplish the insertion normally. It is this property that permits SAFE_INSERT to reiterate the call of INSERT when the first call fails.


14.3.5 Example of Last Wishes

The occurrence of an exception causes termination of the procedures in the dynamic chain of calls up to (and not including) the first procedure that handles the exception. Assume, for example, that A handles a given exception, that B, C and D do not, and that:
A calls B,     B calls C,     C calls D

Then if the exception is raised while executing D, the execution of D is abandoned; then that of C and of B, in that order. (The propagation occurs in the reverse of the order of calls.)

One may want to let these procedures express their last wishes before being abandoned - for instance, to perform some cleanup actions. This can be achieved by providing a handler for others in each of these procedures. Each such handler will then issue the statement

    raise;

which raises the same exception to the attention of the calling procedure. (We are thus able to achieve an effect similar to that of the unwind clause of Mesa without the need for a special language construct.) We illustrate last wishes with the example of a procedure that performs operations on a file:
procedure OPERATE(NAME :  STRING) is
  FILE :  FILE_TYPE;
begin
  -- initial actions
  OPEN(FILE,  INOUT_FILE,  NAME);
  -- perform work on the file
  CLOSE(FILE);
  -- final actions
end;

Should an exception occur during operations on the file, the file would be left in an open state when the body of OPERATE is left. This is avoided by expressing the appropriate corrective action in a handler:
procedure SAFE_OPERATE(NAME :  STRING) is
  FILE :  FILE_TYPE;
begin
  -- initial actions
  OPEN(FILE,  INOUT_FILE,  NAME);
  begin
    -- perform work on the file
  exception
    when others =>
      CLOSE(FILE);
      raise;
  end;
  CLOSE(FILE);
  -- final actions
end;

Now if any exception occurs, either during the initial or the final actions, it will be propagated to the caller of SAFE_OPERATE, and the file will at that time be closed. If however the exception occurs within the block, while performing work on the file, the inner handler will first close the file before propagating the exception.

Similar techniques can be used in parallel processing examples: a given task should not be left waiting forever to receive the stop signal from a task whose execution was abandoned after it sent a start signal.


¤ NEXT ¤ PREVIOUS ¤ UP ¤ TOC ¤ INDEX ¤
Address any questions or comments to adainfo@sw-eng.falls-church.va.us.