Previous

Contents

Next

Chapter 7:
Exceptions

To err is human, to forgive, divine.
— Alexander Pope, An Essay on Criticism


7.1 The importance of exception handling
7.2 Declaring exceptions
7.3 Re-raising exceptions
7.4 Getting information about exceptions
7.5 File input/output
Exercises

7.1 The importance of exception handling

Exception handling was dealt with briefly at the end of chapter 3, but it’s such an important topic that it’s worth looking at in more detail. The Ada exception handling mechanism divides error handling into two separate concerns. One of them is detecting errors and the other is dealing with the errors in an appropriate way, and the two should be treated as two completely separate aspects of error handling. It also makes your programs easier to construct as well as more readable; a procedure is written as a section which deals with processing valid data and a separate section which deals with what to do when things go wrong. Thus, when you’re writing the main part of the procedure you don’t have to worry about how to deal with errors, and when you’re reading it you don’t have to get bogged down in the complexities of the error handling until after you’ve read and understood what happens with correct data. One way of writing a program is to do it incrementally: write the program without worrying too much about error handling initially, test it to make sure it works with correct data, and then concentrate on improving the exception handling once everything else is working. Debugging might also reveal exceptions that are raised in situations that you’ve overlooked, but it is much easier to add extra exception handlers than it is to disturb existing code and then have to go back and test it all again.

This separation of concerns is particularly important when designing packages which could be used by several different programs. It’s always tempting to try and deal with errors as soon as you detect them, but one of the basic rules of package design is that you should never try to handle any errors within the package itself. The package may be able to detect errors but it will not usually know how to deal with them. Handling errors is something which is normally dependent on the overall program, and a package never knows anything about the program that is using it. What may be appropriate in one program may be totally inappropriate in another, and building in any assumptions about how an error should be handled will prevent you from reusing the package in more than one program. Displaying an error message on the screen and then halting may be appropriate in some situations, but in other situations there may not be a screen (e.g. your package is used by a program which controls a washing machine) or it may be a bad idea to halt (e.g. the package is used by a program in an aircraft’s navigational system). Instead you should define your own exceptions and raise them if an error is detected; this will allow the main program to decide how best to deal with the error.


7.2 Declaring exceptions

Ada defines four standard exceptions. You’ve already met Constraint_Error; this is raised whenever a value goes outside the range allowed by its type. You’re very much less likely to meet the others (Storage_Error, Tasking_Error and Program_Error). Storage_Error is raised when you run out of memory. This is only likely to happen when your program is trying to allocate memory dynamically, as explained in chapter 11. Tasking_Error can occur if a program is composed of multiple tasks executing in parallel, as explained in chapter 19, and a task can’t be started for some reason or if you try to communicate with a task that has finished executing. Program_Error is raised in a variety of situations where the program is incorrect but the compiler can’t detect this at compile time (e.g. run-time accessibility checks on access types as explained in chapter 11, or reaching the end of a function without executing a return statement).

Ada allows you to define your own exceptions in addition to the standard exceptions, like this:

    Something_Wrong : exception;

This declares an exception called Something_Wrong. The standard exceptions are of course declared in the package Standard, just as the standard types like Integer are. Other exceptions are defined in other packages such as Ada.Text_IO; Data_Error is an example of this. You may have noticed that Data_Error is not in the list of standard exceptions above. It is actually declared in a package called Ada.IO_Exceptions, and redeclared by renaming inside Ada.Text_IO (and all the other input/output packages) like this:

    Data_Error : exception renames Ada.IO_Exceptions.Data_Error;

Although an exception declaration looks like a variable declaration, it isn’t; about the only thing you can do with an exception (apart from handling it when it is raised) is to raise it using a raise statement:

    raise Something_Wrong;

When you raise an exception, the system looks for a handler for that exception in the current block. If there isn’t one, it exits from the block (going to the line after end in the case of a begin ... end block, or returning to where it was called from in the case of a procedure or function body) and looks for a handler in the block it now finds itself in. In the worst case where there is no handler anywhere it will eventually exit from the main program, at which point the program will halt and an error will be reported.

Note that if an exception is raised inside an exception handler, you exit from the block immediately and then look for an exception handler in the block you’ve returned to. This prevents you getting stuck in an endless exception handling loop. The same thing happens if an exception is raised while elaborating declarations in a declaration section; this avoids the possibility of an exception handler referring to a variable that hasn’t been created yet. Until you’ve got past the begin at the start of the block you’re not counted as being inside it and hence not subject to the block’s exception handlers; once an exception occurs and you’ve entered the exception handler section, you’re counted as having left the block so once again you’re not subject to that block’s exception handlers. In other words, the exception handler only applies to the statements in the body of the block between begin and exception.


7.3 Re-raising exceptions

Sometimes you will want to do some tidying up before exiting a block even if you don’t actually want to handle the exception at that point. For example, you may have created a temporary file on disk which needs to be deleted before you exit from the block. Here’s how you can deal with this situation:

    begin
        -- create a temporary file
        -- do something that might raise a Constraint_Error
        -- delete the temporary file
    exception
        when Constraint_Error =>
            -- delete the temporary file
            raise Constraint_Error;
    end;

The temporary file will be deleted whether an exception occurs or not, either in the course of normal processing or from within the exception handler. A raise statement is used inside the exception handler to raise the same exception again, so that you will immediately exit from the block and look for another handler to handle the exception properly.

Sometimes you don’t know exactly which exception has occurred. If you have an others handler or a single handler for several different exceptions, you won’t know which exception to raise after you’ve done your tidying up. The solution is to use a special form of the raise statement which is only allowed inside an exception handler:

    begin
        -- create a temporary file
        -- do something that might raise an exception
        -- delete the temporary file
    exception
        when others =>
            -- delete the temporary file
            raise;                                -- re-raise the same exception
    end;

Raise on its own will re-raise the same exception, whatever it might be.


7.4 Getting information about exceptions

You may want to print out a message which says what the exception was as part of the handler. There is a standard package called Ada.Exceptions which contains some functions to give you this sort of information. Ada.Exceptions defines a data type called Exception_Occurrence and provides a function called Exception_Name which produces the name of the exception as a string from an Exception_Occurrence. You can get a value of type Exception_Occurrence by specifying a name for it as part of your exception handler:

    begin
    ...
    exception
        when Error : Constraint_Error | Data_Error =>
            Put ("The exception was ");
            Put_Line ( Exception_Name(Error) );
    end;

The name of the Exception_Occurrence is prefixed to the list of exceptions in the handler (the name chosen was Error in this case). There are some other useful functions like Exception_Name; in particular, Exception_Message produces a string containing a short message giving some details about the exception, and Exception_Information produces a longer and more detailed message. Exception_Occurrence objects can also be useful for passing exception information to subprograms called from within an exception handler.

The standard exceptions will have a standard message associated with them. If you want to supply a message for an exception that you’ve defined yourself (or supply a different message for an existing exception) you can use the procedure Raise_Exception:

    Raise_Exception (Constraint_Error'Identity, "Value out of range");

This has the same effect as ‘raise Constraint_Error’ except that the message ‘Value out of range’ will be associated with the exception occurrence. Since an exception is not a data object, you can’t use an exception as a parameter to a subprogram. You can get a data object representing an exception using the Identity attribute which produces a valuewhich produces a of type Ada.Exceptions.Exception_Id, and it is this value which is passed as the first parameter to Raise_Exception..


7.5 File input/output

One of the major sources of exceptions is when dealing with input and output. Users will inevitably supply invalid input from time to time, due to typing errors if nothing else, and your program must be prepared to cope with this. A typical situation arises when a user types in the name of a file that the program is supposed to read some data from or write something to; the filename might be misspelt, the file might be in another directory or on another disk, the disk might be full, the directory might be write protected. In these cases it is often unfair just to terminate the program; the user should generally be given another chance to type a filename in again.

To illustrate this, I’ll briefly describe how file input/output works in Ada. File input/output is fundamentally no different to dealing with the keyboard and screen. The Text_IO package provides all the necessary facilities. The main difference is that you need to open a file before you can use it, and you must close it when you’ve finished using it. To open a file you first of all have to declare an object of type File_Type (defined in Ada.Text_IO):

    File : Ada.Text_IO.File_Type;

Now you can open the file using the procedure Open:

    Open (File, Mode => Ada.Text_IO.In_File, Name => "diary");

This opens an input file whose name is diary. The Mode parameter is an enumeration with three possible values. In_File means the file is to be opened for input, as in this case. Out_File means the file is to be opened for output. Any existing contents of the file will be lost in this case; Out_File specifies that the existing contents of the file should be scrapped. If you don’t want to do this you can use Append_File, which means that whatever output you write to the file will be appended to the end of the file’s existing contents (if any). If the file doesn’t already exist, Open will generate a Name_Error exception. If you want to create a brand new file for output, you can use the procedure Create:

    Create (File, Name => "diary");

This will create a new output file with the given name if it doesn’t already exist, or destroys the existing contents of the file if it does exist. You can optionally supply a Mode parameter as with Open; the default is Out_File, but you might want to use Append_File instead so that you will append your output to the file if it already exists. If the name isn’t legal for some reason a Name_Error exception will be raised; for example, some systems can’t handle filenames containing asterisks or question marks. The other exceptions that can occur when you try to open or create a file are Status_Error, which indicates that the file is already open, and Use_Error, which is raised if you can’t open or create the file for any other reason (e.g. if there is no more disk space).

When you’ve finished using a file you should close it by calling the procedure Close:

    Close (File);

While the file is open you can use Get to read it if it’s an input file and Put or Put_Line to write to it if it’s an output file. The only difference from using the keyboard and the screen is that you have to specify the file you want to read from or write to as the first parameter:

    Get (File, C);             -- get a character from File into C
    Put (File, "xyz");         -- write a string to File
    Put_Line (File, "xyz");    -- same, and then start a new line
    New_Line (File);           -- start a new line in File
    Skip_Line (File);          -- skip to start of next line of File

If you try to read from a file when you’ve reached the end of it, an End_Error exception will be raised. To avoid this you can test if you’re at the end of the file using the function End_Of_File, which is defined in Ada.Text_IO like this:

    function End_Of_File (File : File_Type) return Boolean;

This returns the value True if you’re at the end of the file.

To illustrate how exception handling is used with file I/O, here’s an example program which counts the number of words in a file:

    with Ada.Text_IO, Ada.Integer_Text_IO;
    use Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Word_Count is
        File    : File_Type;
        Name    : String(1..80);
        Size    : Natural;
        Count   : Natural := 0;
        In_Word : Boolean := False;
        Char    : Character;
    begin
        -- Open input file
        loop
            begin
                Put ("Enter filename: ");
                Get_Line (Name, Size);
                Open (File, Mode => In_File, Name => Name(1..Size));
                exit;
            exception
                when Name_Error | Use_Error =>
                    Put_Line ("Invalid filename -- please try again.");
            end;
        end loop;
     
        -- Process file
        while not End_Of_File (File) loop
            -- The end of a line is also the end of a word
            if End_Of_Line (File) then
                In_Word := False;
            end if;
     
            -- Process next character
            Get (File, Char);
            if In_Word and Char = ' ' then
                In_Word := False;
            elsif not In_Word and Char /= ' ' then
                In_Word := True;
                Count   := Count + 1;
            end if;
        end loop;
     
        -- Close file and display result
        Close (File);
        Put (Count);
        Put_Line (" words.");
    end Word_Count;

The program is divided into the three traditional parts: initialisation (open the file), main processing (process the file) and finalisation (close the file and display the results). Opening the file involves getting the name of the input file from the user and then attempting to open it. This is done in a loop, and the loop is exited as soon as the input file is successfully opened. If attempting to open the file raises an exception, the exception handler for the block inside the loop displays the error message; the loop will then be executed again to give the user a chance to type in the filename correctly.

Once the file has been opened, the main processing loop begins. A variable called In_Word is used to keep track of whether we are in the middle of processing a word; initially it’s set to False to indicate that we’re not processing a word. A non-space character means that the start of a word has been seen, so In_Word is set True and the word count in Count is incremented. Once inside a word, characters are skipped until the end of the current line is reached or a space is read, using the function End_Of_Line to test if the current position is at the end of a line of input. Either of these conditions signals the end of a word, so In_Word gets set back to False. When the end of the file is reached, the loop terminates. The input file is then closed and the value of Count is displayed.


Exercises

7.1 Modify the guessing game program from exercise 5.1 to provide comprehensive exception handling to guard against input errors of any kind.

7.2 Write a program which asks the user for the name of an input file and an output file, reads up to 10000 integers from the input file, sorts them using the Shuffle_Sort procedure from the previous chapter, and then writes the sorted data to the output file. Check it to make sure it copes with errors arising from non-existent input files, write-protected destinations for output files, illegal filenames, and (one that often gets overlooked) using the same name for both files.

7.3 Modify the packages JE.Dates from the end of chapter 4 to define an exception called Date_Error, and get the Day_Of function to raise a Date_Error exception if it is called with an invalid date.

7.4 Modify the playing card package from exercise 6.3 to define an exception which will be raised if you try to deal a card from an empty pack or replace a card in a full pack. Use the package to implement a simple card game called ‘Follow The Leader’, in which a human player and the computer player are dealt ten cards each. The object of the game is to get rid of all the cards in your hand. The first player lays down a card, and each player in turn has to play a card which matches either the suit or the value of the previously played card (e.g. the Jack of Clubs could be followed by any Jack or any Club). If a player has no card that can be used to follow on, an extra card must be taken from the pack. If the pack becomes empty, the cards that were previously played (except the last one) must be returned to the pack. The first player to play out a hand and have no cards left is the winner.



Previous

Contents

Next

This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright © John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2002/02/22 01:47:18 $