When an error occurs during the elaboration or execution of a statement, Ada is said to raise an exception. Ordinarily this stops the program, but Ada programs can trap exceptions and execute a special block of code when an exception is raised. This code is called an exception handler.
We can define our own exceptions, but four of them are predefined by Ada:
- This is the exception encountered most often by beginners, because it can be caused by a number of different things. It can be raised by a subscript out of range, a subtype out of range (USA.Day := 32;), an attribute used improperly (Integer'Value("12X3") or Month_Type'Val(13)), assigning an array of one length to a destination of another (H : String(1 .. 5) := "Hi";), attempting to access an object through a null access value (D1 := null; D1.Day := 31;), attempting to divide by zero, or by an arithmetic overflow.
- This is rarely encountered by beginners, but it can be raised by skipping around the return statement in a function and running into the end statement.
- This is raised by running out of memory, as with a recursive program calling itself unconditionally or an attempt to create an infinitely large linked list.
- This will be discussed in the section on Tasking.
(Older Ada 83 compilers also predefined an exception Numeric_Error, but now Numeric_Error is simply another name for Constraint_Error.)
An exception handler is introduced by the reserved word exception; its structure is similar to that of a case construct. We'll see an example in a moment. Unlike a case construct, an exception handler need not account for all the possibilities.
An exception handler can be placed in a subprogram, in the initialization code of a package, in a task (to be discussed in the section on Tasking), or in a block (to be discussed later in this section). Here's a procedure with an exception handler that handles an exception, Wrong, that we declare ourselves, as well as the built-in exceptions Constraint_Error and Numeric_Error:
with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Exception_Demo is I : Integer; Wrong : exception; begin loop New_Line(2); Put("Type a positive integer. "); Get(I); Skip_Line; if I <= 0 then raise Wrong; end if; Put("The square is ... "); -- Raises Constraint_Error or Numeric_Error -- if I is too large. Put(I*I); end loop; exception when Constraint_Error | Numeric_Error => Put(" ... too big."); when Wrong => New_Line; Put("I said Positive integer!"); end Exception_Demo;
We can deliberately raise an exception (either user-defined or built-in) with the raise statement, as in raise Wrong; or raise Constraint_Error; . Also, ordinary statements can raise exceptions. In our sample program, Put(I*I); raises Constraint_Error or Numeric_Error if I is too large. When an executable statement raises an exception, the exception handler is executed instead of the rest of the procedure, function, etc. Our program keeps asking for integers and displaying their squares until an exception is raised. Then, the exception handler is executed, and there's no way to get back into the procedure to ask for another integer (short of a recursive call). Even a goto from the exception handler to the main part of the procedure is forbidden. Soon we'll show how the block construct can overcome this problem, so that our program will continue to ask for more integers even after an exception is handled.
As with case constructs, an exception handler may use the vertical bar to denote multiple choices (whenb Constraint_Error | Numeric_Error => block of code), and it may say when others => to handle all cases not covered earlier. But there's no way to test, inside the exception handler, which line raised the exception. We can only test which kind of exception was raised.
Don't use exceptions where a simple if will do. In our program, trapping the arithmetic overflow was OK, but the if could have handled I b<= 0 without raising Wrong. This exception was declared only to give a simple example.
QuestionAssuming Rainbow_Color and Traffic_Light_Color are defined as before, which of the below exceptions would be raised by Rainbow_Color'Value("AMBER")?
You're right! Using an attribute improperly in this way will raise Constraint_Error.
No, Numeric_Error is usually raised by arithmetic overflow, or attempted division by zero, with older Ada compilers (modern compilers raise Constraint_Error instead).
No, Program_Error is usually raised by skipping around the return statement in a function.
No, Storage_Error is raised by running out of memory.
No, Tasking_Error is raised only by programs using tasking, which we haven't yet discussed.
A block construct lets us declare objects in the executable region of the program. For example, in the following, I and F come into existence where they're declared, and go out of existence at the following end statement:
procedure Block_Demo is Q : Float; begin Q := 0.0; declare I : Integer; F : Float; begin I := 5; F := Q; end; Q := Q + 3.0; end Block_Demo;
However, the usual use of a block is to localize an exception handler, not to bring objects into existence in the executable region of a program. The declarative part of the block is optional. For example, let's rewrite Exception_Demo to make use of a block with an exception handler.
with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Exception_Demo is I : Integer; Wrong : exception; begin loop begin New_Line(2); Put("Type a positive integer. "); Get(I); Skip_Line; if I <= 0 then raise Wrong; end if; Put("The square is ... "); Put(I*I); exception when Constraint_Error | Numeric_Error => Put(" ... too big."); when Wrong => New_Line; Put("I said Positive integer!"); end; end loop; end Exception_Demo;
Note that in our rewritten program, a block with an exception handler has been created inside the loop. Now, if an exception occurs, the handler will be executed instead of the rest of the block, not the rest of the procedure. Thus, the loop will still be executed, and the program will continue to ask for integers after an exception is handled.
There are two advantages to confining exception handlers to small blocks. First, we narrow down the range of statements that might have raised the exception. Recall that the handler can't test which line raised the exception, but it must have been one of the lines in the block. (If an exception is raised outside the block, our program provides no handler for it.) Second, program execution will continue after the end of the block.
If an exception occurs for which there's no handler, the exception reaches the next higher level. For example, if the block in Exception_Demo somehow raises Storage_Error and doesn't handle it, an exception handler for the whole procedure would get a chance to handle it. (In our case, there is none.) If it's still unhandled, it's as if the call to Exception_Demo raised Storage_Error. If the caller doesn't handle it, the exception reaches the caller's caller, etc. If the exception reaches the main program and is still unhandled, the program is stopped and the system displays the name of the exception. However, exceptions that are handled don't even reach the caller.
In the unusual case of an exception raised in the declarative region, the unit raising the exception (subprogram, block, etc.) is not given a chance to handle it. Exceptions raised in the declarative region immediately reach the next higher level.
In a handler, the word raise may be used without a name of an exception to re-raise whatever exception brought control to the handler. This is especially useful after when others =>, because any one of a number of exceptions might have transferred control there. For example,
when others => Put_Line("I don't know what went wrong."); -- Close files and do general cleanup. raise;
This lets us do some processing of the error, and still lets the next higher level do additional processing. Note that it's superfluous to say simply when others => raise; because the exception will reach the next higher level even if that code is omitted. Any unhandled exception reaches the next higher level.
An error occurring in an exception handler is unhandled and reaches the next higher level (unless it occurs in a block with its own exception handler).
In Ada 95, it's possible to get some information about an exception, even in the when others branch of an exception handler. The package Ada.Exceptions provides a type Exception_Occurrence and three functions (Exception_Name, Exception_Message, and Exception_Information) that take an object of type Exception_Occurrence and return a String. Here's a sample program making use of that feature:
package P is Cain : exception; end P; with P, Ada.Text_IO, Ada.Exceptions; use P, Ada.Text_IO; procedure Test is begin raise Cain; exception when Fault : others => Put_Line(Ada.Exceptions.Exception_Name(Fault)); end Test;
Note that we create an identifier (like Fault) and place it and a colon between when and the name of the exception (or others). This automatically declares the constant Fault to be of the proper type, Exception_Occurrence.
The function Exception_Name returns the full name of the exception, using dot notation. This tells us in which package the exception was declared. For example, the above program displays P.CAIN because Cain was declared in the package P.
The information retrieved by Exception_Message and Exception_Information depends on the particular implementation of Ada 95. However, in the case where we deliberately raise an exception ourselves, we can control the information that will be retrieved by Exception_Message. Instead of executing the raise statement, we call the procedure Raise_Exception in Ada.Exceptions. The first parameter is the name of the exception followed by 'Identity, and the second parameter is the string that we want Exception_Message to retrieve. For example, if exception Cain is defined, instead of raise Cain; we could write Ada.Exceptions.Raise_Exception(Cain'Identity, "That number is too large.");. Then, in the exception handler, the call to Ada.Exceptions.Exception_Message would return the string "That number is too large."
One warning about Storage_Error: When a handler for Storage_Error is reached, the program has already run out of memory. The program may no longer be able to use Ada.Text_IO, Exception_Name, Exception_Message, etc. An attempt to do so may just raise Storage_Error again, bypassing your handler.
with Ada.Text_IO; use Ada.Text_IO; procedure One is procedure Two is separate; begin Two; exception when others => Put_Line("1"); end One; separate (One) procedure Two is Cain : exception; begin raise Cain; exception when others => Put_Line("2"); end Two;What will the above program display?