[Ada Information Clearinghouse]

Ada '83 Quality and Style:

Guidelines for Professional Programmers

Copyright 1989, 1991,1992 Software Productivity Consortium, Inc., Herndon, Virginia.

CHAPTER 4: Program Structure

4.3 Exceptions

This section addresses the issue of exceptions in the context of program structures. It discusses how exceptions should be used as part of the interface to a unit, including what exceptions to declare and raise and under what conditions to raise them. Information on how to handle, propagate, and avoid raising exceptions is found in Guideline 5.8. Guidelines on how to deal with portability issues are in Guideline 7.5.

Language Ref Manual references: 7.1 Package Structure, 11 Exceptions

In this section...
4.3.1 Using Exceptions to Help Define an Abstraction
Summary of Guidelines from this section


4.3.1 Using Exceptions to Help Define an Abstraction

guideline

example

This package specification defines an exception which enhances the abstraction:
------------------------------------------------------------------------- 
generic 
   type Element is private; 
package Stack is

   function Stack_Empty return Boolean;
   
   -- Raised when POP is used on empty stack. 
   No_Data_On_Stack : exception;
   
   procedure Pop  (From_Top :    out Element); 
   procedure Push (Onto_Top : in     Element);
   
end Stack; 
-------------------------------------------------------------------------

This example shows how to convert a predefined exception to a user-defined one:
   ... 
   ---------------------------------------------------------------------- 
   procedure Pop (From_Top :    out Element) is 
      ...
      
      if Stack_Empty then 
          raise No_Data_On_Stack;
   
      else -- Stack contains at least one element 
          Top_Index := Top_Index - 1; 
          From_Top  := Stack(Top_Index + 1);
   
      end if; 
   end Pop;
   
   ---------------------------------------------------------------------- 
   ...

rationale

Exceptions should be used as part of an abstraction to indicate error conditions which the abstraction is unable to prevent or correct. Since the abstraction is unable to correct such an error, it must report the error to the user. In the case of a usage error (e.g., attempting to invoke operations in the wrong sequence or attempting to exceed a boundary condition), the user may be able to correct the error. In the case of an error beyond the control of the user, the user may be able to work around the error if there are multiple mechanisms available to perform the desired operation. In other cases, the user may have to abandon use of the unit, dropping into a degraded mode of limited functionality. In any case, the user must be notified.

Exceptions are a good mechanism for reporting such errors because they provide an alternate flow of control for dealing with errors. This allows error-handling code to be kept separate from the code for normal processing. When an exception is raised, the current operation is aborted and control is transferred directly to the appropriate exception handler.

Several of the guidelines above exist to maximize the ability of the user to distinguish and correct different types of errors. Providing a different exception name for each error condition makes it possible to handle each error condition separately. Declaring new exception names, rather than raising exceptions declared in other packages, reduces the coupling between packages and also makes different exceptions more distinguishable. Exporting the names of all exceptions which a unit can raise, rather than declaring them internally to the unit, makes it possible for users of the unit to refer to the names in exception handlers. Otherwise, the user would be able to handle the exception only with an others handler. Finally, using comments to document exactly which of the exceptions declared in a package can be raised by each subprogram or task entry making it possible for the user to know which exception handlers are appropriate in each situation.

Because they cause an immediate transfer of control, exceptions are useful for reporting unrecoverable errors which prevent an operation from being completed, but not for reporting status or modes incidental to the completion of an operation. They should not be used to report internal errors which a unit was able to correct invisibly to the user.

To provide the user with maximum flexibility, it is a good idea to provide interrogative functions which the user can call to determine whether an exception would be raised if a subprogram or task entry were invoked. The function Stack_Empty in the above example is such a function. It indicates whether No_Data_On_Stack would be raised if Pop were called. Providing such functions makes it possible for the user to avoid triggering exceptions.

To support error recovery by its user, a unit should try to avoid changing state during an invocation which raises an exception. If a requested operation cannot be completely and correctly performed, then the unit should either detect this before changing any internal state information, or should revert back to the state at the time of the request. For example, after raising the exception No_Data_On_Stack, the stack package in the above example should remain in exactly the same state it was in when Pop was called. If it were to partially update its internal data structures for managing the stack, then future Push and Pop operations would not perform correctly. This is always desirable, but not always possible.

User-defined exceptions should be used instead of predefined or compiler-defined exceptions because they are more descriptive and more specific to the abstraction. The predefined exceptions are very general, and can be triggered by many different situations. Compiler-defined exceptions are nonportable and have meanings which are subject to change even between successive releases of the same compiler. This introduces too much uncertainty for the creation of useful handlers.

If you are writing an abstraction, remember that the user does not know about the units you use in your implementation. That is an effect of information hiding. If any exception is raised within your abstraction, you must catch it and handle it. The user is not able to provide a reasonable handler if the original exception is allowed to propagate out. You can still convert the exception into a form intelligible to the user if your abstraction cannot effectively recover on its own.

Converting an exception means raising a user-defined exception in the handler for the original exception. This introduces a meaningful name for export to the user of the unit. Once the error situation is couched in terms of the application, it can be handled in those terms.

Do not allow an exception to propagate unhandled outside the scope of the declaration of its name, because then only a handler for others can catch it. As discussed under Guideline 5.8.2, a handler for others cannot be written to deal with the specific error effectively.

Language Ref Manual references: 6.1 Subprogram Declarations, 6.5 Function Subprograms, 7.2 Package Specifications and Declarations, 7.3 Package Bodies, 9.5 Entries, Entry Calls, and Accept Statements, 11.1 Exception Declarations, 11.2 Exception Handlers, 11.3 Raise Statements, 11.4 Exception Handling


Back to document index