Athena

Part 8: Exception Handling

Brian Long (www.blong.com)

This month Brian Long looks at how to add error handling to your Kylix applications..


This article first appeared in Linux Format Issue 26, April 2002.

Click here to download the files associated with this article.

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

A natural part of writing any useful application is writing the error handling code. Different programming languages offer different capabilities in this regard and both this and next month are going to be spent looking at what Object Pascal in Kylix (and also in Delphi) brings to the table.

Error Handling Without Exceptions

In many older languages, such as C, the normal job of detecting errors and responding to them is entirely down to the user. As you write your lines of code many of them will involve function calls. Most functions will validate their input and behaviour and will return special indicative values if something goes wrong (such as -1 or False). Sometimes they will also set a global error variable to a value that indicates specifically what went wrong.

This is just what the Linux programming API (as defined in the glibc library and made available to Kylix programmers through the Libc import unit) does. Failing Linux system routines return -1 and you find the actual error code from errno.

As a consequence, any line of code that calls such a function is required to be written in conjunction with a conditional statement to cater for both normal operation and the error situation. As you write more code you must also write more and more conditions and the logic you are trying to implement can become somewhat obscured thanks to all this branching code. This in itself can lead to maintenance issues as the code and error handling gets more involved.

Something that might make all this more manageable is if the onus for executing the error handling code was taken away from the programmer and was left with the language itself. If the language could recognise when an error occurred and jump straight to the error handler the programmer's logic would be more straightforward and clear cut (and more maintainable). The whole mess of conditional statements that ensure the right code is executed at the right point depending on whether an error occurs or not would disappear. The programmer would be left with the job of writing simple statements, one after the other, without having to constantly check to see if an error occurred in the previous statement or not.

This is where exceptions come into the story.

A Recap On Kylix Error Handling

Figure 1: A simple app to show automatic error handling

Before looking at how exceptions affect error handling, let's have a quick look at an application that has some errors in it. In this month's files you'll find the AutomaticErrorHandling.dpr project that looks like Figure 1 when running. Each of the five buttons is deliberately set up to cause a completely different type of error and you can see the button event handlers below.


procedure TForm1.Button1Click(Sender: TObject);
begin
  Tag := StrToInt(Application.ExeName)
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  (Sender as TForm).Hide
end;

procedure TForm1.Button3Click(Sender: TObject);
var
  Lst: TListBox;
begin
  Lst := TListBox.Create(Self);
  Lst.Parent := Self;
  Lst.SendToBack;
  Lst.Items.LoadFromFile('Non-existentFile.txt');
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  //SIGFPE signal
  Tag := 100 div Tag
end;

procedure TForm1.Button5Click(Sender: TObject);
var
  PI: PInteger;
begin
  PI := nil;
  //SIGSEGV signal
  Caption := IntToStr(PI^)
end;

The first button uses StrToInt to turn a string representation of an integer number into an actual integer value. The result is to be assigned to the form's Tag property, but this will never actually happen as the source string, Application.ExeName, will never represent a valid number (it holds a fully qualified path to the application's executable file. Incidentally, all components have an integer Tag property, which defaults to 0 and can be used for any purpose you like (the CLX library never uses Tag).

The second button takes its event handler's Sender parameter (a reference to the object that triggered the event, which will actually be a TButton) and erroneously tries to dynamically cast it into a TForm reference in order to call the Hide method.

The third button dynamically creates a list box component which appears on the form. To ensure it doesn't obscure the buttons it is sent to the back of the Z order (it is drawn first and then the buttons will be drawn over it). Then it tries to fill the list box with lines from a non-existent text file, which causes the error.

Figure 2: Your program dealing with an error all by itself

If you test these three buttons, each one causes an error and the program reports it in a simple message box (shown in Figure 2). When you clear the message box the program carries on unharmed.

Figure 3: The debugger telling you about an exception in your program

Note that if the default debugging options are still set, the debugger will intercept each error and inform you of its presence (as you can see in Figure 3), suspending the program as it does so (you can choose Run | Run or press F9 to let the program continue after closing the debugger's message dialog). You can stop this from happening if need be by choosing Tools | Debugger Options..., selecting the Language Exceptions page and unchecking the Stop on Delphi Exceptions checkbox (see Figure 4).

Figure 4: Stopping the debugger from intercepting software exceptions

The point of this application is to show that when any of these completely different types of errors occur your application will report the error in a consistent and recoverable manner. This is without you having to write any code or even realise the errors might occur.

This first group of buttons all generate software errors (logical errors in the program) but the next two take it a stage further.

Hardware Errors And Signals

The fourth button performs an invalid arithmetic operation (a divide by zero). To get this error to happen it is not possible to simply write an expression such as:

100 / 0

or

I div 0

as the compiler will instantly reject such statements (it checks for division by a literal value of 0). So instead the Tag property is used again (which, if you recall, defaults to 0).

The difference between the / and div operator, which both do division, is the type of value generated. / generates a floating point result, but you cannot directly assign a floating point value to an integer variable or property (the compiler refuses due to the fact you would lose the fraction part of the result). If you really wanted to, you could pass the result to either the Trunc or Round BaseCLX routines to truncate or round the value to an integer. The general approach, though, is to use the div operator instead, which performs integer division. The complimentary mod operator can be used to get the remainder, so 5 div 3 gives a result of 1 and 5 mod 3 gives a result of 2.

The divide by zero error is actually picked up by the processor and so represents a hardware error. Linux reports the error to the application as a SIGFPE error.

The last button is designed to induce a nastier type of error. It declares a local variable that is defined as a pointer to an integer (if you are unfamiliar with the use of pointers don't worry too much about it as most Kylix applications don't have cause to use them). The pointer is set to nil (meaning it holds address 0) and then de-referenced to get an integer to pass to IntToStr. This means the integer that is supposedly in memory at address 0 is read and passed to the function. However, when the CPU is running in protected mode (as it is in Linux and Windows) it performs extra validation of certain machine operations. For example, every memory access is examined to ensure the program has appropriate rights to do it. If this is not the case, the CPU raises a special hardware signal to stop the program from proceeding any further, which Linux translates into a SIGSEGV signal.

No applications have permission to read from memory address zero so this application will fall foul of the CPU's watchful eye.

Figure 5: The debugger informing you of a signal received by your program

When an application running under the debugger receives a Linux signal, the IDE again pops up to alert the developer (see Figure 5). You can kill the debugger's interest in signals in a similar way to how we did it for language exceptions. In the debugger options dialog, the Signals page lists all the known Linux signals. You can select those of interest here (SIGFPE and SIGSEGV) and change their Handled by option from Debugger to User program (see Figure 6).

Figure 6: Stopping the debugger's interest in signals

Unfortunately, Kylix 1 seems to have a problem with its signal handling in that signals tend to kill applications dead (left hanging) and don't get very far in the debugger either (the signal report will keep recursively appearing if enabled, otherwise the debuggee will hang).

Fortunately, Kylix 2 and 3 seem to have better signal handling. The debugger will still notify you when a signal occurs (if the option is enabled) but the application then continues to handle the error quite successfully, reporting it as an Access Violation (see Figure 7). Kylix 3 Open Edition is available for download from Borland's Web site (http://www.borland.com/products/downloads/download_kylix.html).

Figure 7: A Kylix program reporting an Access Violation

Error Handling With Exceptions

Object Pascal (and other languages such as C++ and Ada) support the notion of exceptions. Exceptions are representations of errors that are understood by the language itself. In Object Pascal all exceptions are objects that sit in a branch of the class hierarchy. The root exception class is Exception and many other exception classes inherit from it such as EConvertError and EInvalidCast. The base exception class happens to start with an E and all the inherited classes use an E prefix by convention.

When code is executing and an exception occurs to indicate a problem exists (we'll see how to do this in your own code next month) the execution automatically jumps to the closest error handler for that exception. As mentioned before, this makes the job of writing code much more straightforward and lets you forget about the constant checking for errors that other languages oblige you to make.

As you have seen, if you don't write an error handler the program responds with a default message box. If you want to respond to the error in a different way the language has constructs to achieve this.

The inclusion of exceptions in Object Pascal makes writing a robust application much simpler than it could be. When you want to write error handling code, it is separated from the logic to which it applies making both the logic and the error handling easier to read and maintain.

The Syntax Of Exception Handling

You handle specific exceptions in a code block using a special compound statement that comprises two sections. The first section in the try..except statement contains the code you wish to handle errors for and the second holds the error handlers themselves (called exception handlers). Exception handlers each respond to exceptions of a specified class type (and those inherited from that class) so you need to know the exception to handle in advance (when the debugger responds to an exception it tells you this information as you can see in Figure 3).

What you do in the exception handler is entirely up to you, but it could be as simple as displaying a different message box with a more user-friendly error message. For example, consider some code that gets a radius value from the user and then displays the area of a circle with that radius and also the volume of a sphere with the same radius. You could write it like this (the code is in the ExceptionHandling.dpr project in this month's files):


procedure TForm1.Button1Click(Sender: TObject);
var
  Radius, Area, Volume: Double;
  RadiusStr: String;
begin
  RadiusStr := '1.0';
  if InputQuery('Trigonometry 101', 'Enter a radius', RadiusStr) then
  begin
    Radius := StrToFloat(RadiusStr);
    Area := Pi * Sqr(Radius);
    Volume := Pi * Power(Radius, 3) * 4/3;
    ShowMessageFmt(
      'A circle with radius %g has area %g'#13#10 +
      'A sphere with radius %g has volume %g.'#13#10 +
      'Calculation performed at %s on %s',
      [Radius, Area, Radius, Volume,
       TimeToStr(Time), DateToStr(Date)])
  end
end;

InputQuery displays a simple entry dialog with a caption and prompt as specified by the string constants passed to it and an edit control with the value of the third string parameter written in it. If the user presses OK their new value is returned in this variable and InputQuery returns True, otherwise it returns False.

ShowMessageFmt takes the same parameters as Format, which builds a formatted string, and passes it to ShowMessage to display it, as shown in Figure 8 (in fact there is also an overloaded version of ShowMessage that takes the same parameters). Format was used in the code last month and is a convenient way to build formatted strings. You supply a template string with placeholders to represent different types of values (such as %g for a floating point number and %s for a string) and then supply a list of expressions whose values get plugged in. You can find more information about these placeholders in the help (look up Format function and Format strings).

Figure 8: Correct behaviour from the app

The Power function and many other useful maths routines are implemented in the Math unit so you must add Math to your uses clause before this code will compile.

Figure 9: The default EConvertError message

StrToFloat generates an EConvertError exception if it goes wrong (such as being passed a string that doesn't represent a float) and the default error message look like Figure 9. You could choose to make a more friendly error message by changing the code to this:


uses
  Math;
...
procedure TForm1.Button1Click(Sender: TObject);
var
  Radius, Area, Volume: Double;
  RadiusStr: String;
begin
  RadiusStr := '1.0';
  if InputQuery('Trigonometry 101', 'Enter a radius', RadiusStr) then
    try
      Radius := StrToFloat(RadiusStr);
      Area := Pi * Sqr(Radius);
      Volume := Pi * Power(Radius, 3) * 4/3;
      ShowMessageFmt(
        'A circle with radius %g has area %g'#10 +
        'A sphere with radius %g has volume %g'#10 +
        'Calculation performed at %s on %s',
        [Radius, Area, Radius, Volume,
         TimeToStr(Time), DateToStr(Date)])
    except
      on EConvertError do
        MessageDlg(
          'Your entry was not recognised as a valid number',
          mtInformation, [mbOk], 0)
    end
end;

If any code in the try block generates an EConvertError exception (in truth the StrToFloat call is the only likely line) then execution immediately jumps to the EConvertError handler. Neither the calculation statements nor the call to ShowMessageFmt need to be incorporated into a condition that checks if the floating point number was valid, since they would not be executed if this were the case (they would be skipped en routine to the exception handler).

The exception handler in this case simply displays its own error message box containing a different string (Figure 10). But again, you can execute any appropriate code that you want to here.

Figure 10: A Different Response To EConvertError

Note that you can handle as many exceptions as you choose by writing multiple exception handlers:


try
  //code that may cause exceptions
except
  on ExceptionType1 do
    // ExceptionType1 handler
  on ExceptionType2 do
    // ExceptionType2 handler
  //and so on
end

What's In An Exception And How To Get It

The Exception class implements just one useful property for exception handlers to use. The Message property returns the string displayed in the default exception handler's message box.

Some exception classes expose a little more information that can prove useful when writing an exception handler. One example is the EInOutError exception that's generated for file I/O errors when the appropriate compiler switch is enabled. The I/O Checking switch on the Compiler page of the project options dialog controls this and it is enabled by default.

When this option is enabled and an I/O error occurs, such as access being denied or a file not being found, an EInOutError exception is raised and its ErrorCode field (not a property but a data field) is set to an appropriate error code. If the value is between 0 and 99 it represents an OS-specific error (the meanings often differ between Linux and Windows) and if it is between 100 to 149 it represents a platform-independent error generated by BaseCLX. You can find more about these errors in the help under EInoutError or I/O errors.

If you need to access anything in an exception object the syntax used so far is a bit limiting. It tells you that the exception is of the specified class type but does not allow access to the exception object. An optional extension to the syntax overcomes this small issue, as in:


try
  //code that may cause an exception
except
  on E: Exception
    //exception handling code
end

For the duration of the exception handler the exception object is available as E (although any valid identifier can be used), so E.Message tells you the exception description.

Some text file manipulation code that uses the I/O error code is shown below. The code assumes an existing file is chosen in a dialog in order to have a simple line of text appended to it.


uses
  Libc;
...
procedure TForm1.Button2Click(Sender: TObject);
var
  TF: TextFile;
begin
  if dlgSave.Execute then
    try
      AssignFile(TF, dlgSave.FileName);
      Append(TF);
      WriteLn(TF, 'Hello world');
      CloseFile(TF)
    except
      on E: EInOutError do
        case E.ErrorCode of
          ENOENT {2}: ShowMessage('"%s" cannot be located', [dlgSave.FileName]);
          EACCES {13}: ShowMessage('You do not have permission to overwrite this file');
        else
          if E.ErrorCode < 100 then
            ShowMessage('I/O Error'#10#9'Error code: %d'#10#9 +
              'Error msg: %s', [E.ErrorCode, SysErrorMessage(E.ErrorCode)])
          else
            ShowMessage('I/O Error'#10#9'Error code: %d'#10#9 +
              'Error msg: %s', [E.ErrorCode, E.Message]);
        end; //case
    end //try
end; //event handler

The TF variable is used to access the text file whose name is chosen using a TSaveDialog component. If any I/O error occurs an EInOutError exception is raised automatically and so the EInOutError exception handler executes.

The exception handler has a specific response for error codes 2 and 13 (represented by the ENOENT and EACCES constants from the Libc unit) providing user-friendly messages. However for all other error codes a generic message is displayed.

For OS errors, the SysErrorMessage BaseCLX routine is used to get a descriptive message to display along with the error code (Figure 11 shows what happens when you choose your own running executable file). For other error codes the message that came with the exception is coupled with the error code. Both these generic messages (and one of the previous messages) are split over several lines using control codes (#10 is a line feed character and #9 is a tab character).

Figure 11: A custom response to a specific I/O error

Note that, as mentioned before, the error codes below 100 are OS-dependent. The constants used in the case are defined in the Libc unit. In a Delphi application the corresponding OS error constants are defined in the Windows unit (ERROR_FILE_NOT_FOUND has a value of 2 and ERROR_ACCESS_DENIED is 5). You could use conditional compilation directives to write code that is portable between the two platforms as shown below.


case E.ErrorCode of
{$ifdef LINUX}ENOENT{$endif}
{$ifdef MSWINDOWS}ERROR_FILE_NOT_FOUND{$endif}:
   ShowMessage('"%s" cannot be located', [dlgSave.FileName]);
{$ifdef LINUX}EACCES{$endif}
{$ifdef MSWINDOWS}ERROR_ACCESS_DENIED{$endif}:
   ShowMessage('You do not have permission to overwrite this file');
else
  ...
end;

How Exception Handlers Are Located

When an exception occurs in a Kylix application the closest exception handler to it is executed. That means that if the offending statement is in the try block of a try..except statement that contains an exception handler for that exception type, it will be executed.

If not, the routine that called the current one is checked for an exception handler that contains the call. If no luck there the search continues back to the caller of that routine, and so on as long as there are nested calls. At some point an exception handler will be found or the top most routine will have been checked. If no exception handling statement is found then the application's default exception handler takes over and handles the exception as we saw earlier.

If you think about these mechanics you can see that in a complex application with many cases of routines calling other routines there will be cases where an exception occurs in one routine and the exception handler is found several functions further up the function call stack. In order for this to work properly all the intermediate routines (including the one that triggered the exception) have to be voided. It is not possible to return back to where the exception originated from. Kylix therefore implements what is called a non-resumable exception model.

Chaining Exception Handlers

Let's say you have routine A and routine B. Routine A calls routine B and B contains a variety of statements. Due to the possibility of B causing an exception the implementation of A contains a try..except handler to handle the exceptions in some way. Maybe the exception handler logs details of the exceptions to a log file or performs some other useful handling task. The code below shows the picture we are trying to paint:


procedure TForm1.Button3Click(Sender: TObject);
begin
  //more code
  A
  //more code
end;

procedure TForm1.A;
begin
  try
    //more code
    B
    //more code
  except
    on E: EConvertError do
    begin
      ShowMessage('We just logged a problem:'#10#10 +
        'A %s exception occurred, saying:'#10'"%s"',
      [E.ClassName, E.Message])
    end
  end
end;

procedure TForm1.B;
var
  Radius, Area: Double;
  RadiusStr: String;
begin
  RadiusStr := '1.0';
  if InputQuery('Trigonometry 101', 'Enter a radius', RadiusStr) then
  begin
    Radius := StrToFloat(RadiusStr);
    Area := Pi * Sqr(Radius);
    ShowMessageFmt('A circle with radius %g has area %g', [Radius, Area])
  end
end;

Now consider what happens if you come back to routine B some time later and decide to write an exception handler in there to perform some necessary exception handling, perhaps to produce a more user-friendly error message. The result of this is that the exception handling code in A will now stop working as no exceptions are being generated by B that are left unhandled.


procedure TForm1.B;
var
  Radius, Area: Double;
  RadiusStr: String;
begin
  RadiusStr := '1.0';
  if InputQuery('Trigonometry 101', 'Enter a radius', RadiusStr) then
    try
      Radius := StrToFloat(RadiusStr);
      Area := Pi * Sqr(Radius);
      ShowMessageFmt('A circle with radius %g has area %g', [Radius, Area])
    except
      //This kills of A's exception handler - it will never be triggered now
      on EConvertError do
        MessageDlg('Your entry was not recognised as a valid number',
          mtError, [mbOk], 0);
    end
end;

This might not be the effect you want so there is a mechanism available through the reserved word raise to regenerate the same exception in your exception handler as you just caught. This causes the execution to jump to the next closest exception handler. If raise is executed in the exception handler in B it will chain onto the exception handler in A.


procedure TForm1.B;
var
  Radius, Area: Double;
  RadiusStr: String;
begin
  RadiusStr := '1.0';
  if InputQuery('Trigonometry 101', 'Enter a radius', RadiusStr) then
    try
      Radius := StrToFloat(RadiusStr);
      Area := Pi * Sqr(Radius);
      ShowMessageFmt('A circle with radius %g has area %g', [Radius, Area])
    except
      on EConvertError do
      begin
        MessageDlg('Your entry was not recognised as a valid number',
          mtError, [mbOk], 0);
        //This chains back to the next closest exception handler
        raise
      end
    end
end;

In the case of the example code, if an invalid number is entered the error shown in Figure 10 is displayed, followed by a message confirming that the logging code is executing.

Summary

Next month we'll continue looking at exception handling and related issues, including application-wide exception handlers, raising exceptions, custom exceptions and resource protection. In the meantime, if there is something about Kylix Open Edition you want to see covered here, drop us an email and we'll try our best to incorporate it into a future instalment.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware.

If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi 2000 award.


Back to top