HOME - - - - - Delphi Tutorials TOC - - - - - - Other material for programmers
Delicious  Bookmark this on Delicious    Recommend to StumbleUpon

Delphi's "try... except" mechanism
for error trapping and management

This has good information, and a search button at the bottom of the page

Please don't dismiss it because it isn't full of graphics, scripts, cookies, etc!

Click here if you want to know more about the source and format of these pages.

This page is "browser friendly". Make your browser window as wide as you want it. The text will flow nicely for you. It is easier to read in a narrow window. With most browsers, pressing plus, minus or zero while the control key (ctrl) is held down will change the texts size. (Enlarge, reduce, restore to default, respectively.) (This is more fully explained, and there's another tip, at my Power Browsing page.)


Product warning

This tutorial is longer than many for two reasons....

   * It covers a somewhat complicated mechanism.

   * It "digresses" into discussions of programming ideas which arise along the way.

If you just want "the answer", this tutorial may frustrate you. But "the answer" is in the Delphi help file. At least the one that came with Delphi 4, which is what was used to test everything that follows. This tutorial will, I believe, help you to "understand" the answer, and help you become a better programmer.

Make it right, but provide for the unexpected

In general, your code should be built so carefully that there isn't room for unexpected events. And it should be so elegant and logical that when things go wrong, WHAT has gone wrong is easy to discern. Yeah.

Because Life isn't Like That, there's the "Try... Except" structure. It allows you to provide an exit of your choice and design when things go wrong.

To explore exception handling, we're going to create a little application. Please don't be offended by the artificiality of what we're doing... if we KNEW where our bugs were, we wouldn't leave them in our code, would we? So any tutorial example is bound to be somewhat artificial, isn't it?

We're going to start with something which shouldn't go wrong. Then we'll change it slightly, so that it goes wrong from time to time, then change it again, using exception handling to deal with the problems which arise when it goes wrong.

We will have a form with some text on it. A timer will cause the application to pick a number every second, and use it in a simple bit of arithmetic.

The text will be in two labels. The first will say something like....

250 plus 2 is

And the second will announce the answer.

The first program. Simple, and works

Here we go....

To exactly duplicate what I am going to do below, you would save the project in a folder called DD96, name the unit DD96u1.pas, name the form DD96f1, and save the project as DD96.dpr. Not "necessary", but probably worth doing, "for the simple life".

Set up a form with a "Quit" button, a timer, and two labels, laSum and laAns.

Create three global variables...

bNum1,bNum2:byte;
bAns:byte;

Let's build our program gently. In the first iteration, it won't use the timer I promised you. It will just do 250 plus 2 and stop.

In the FormCreate handler...

bNum1:=250;
bNum2:=2;
UpdateDisplay;

... which won't work yet, because UpdateDisplay isn't a built-in procedure... we are about to add it. As a "baby step" towards that, create an initial version of UpdateDisplay as follows:

procedure TDD96f1.UpdateDisplay;
begin
bAns:=bNum1+bNum2;
laSum:=inttostr(bNum1)+' plus '+inttostr(bNum2)+' equals...';
laAns.caption:=inttostr(bAns);
end;

The whole code for what we've created so far is as follows. If you study it, you will find a few "bonus" ideas, little "housekeeping" provisions which I put in all of my applications.

unit DD96u1;

(*Illustration of using try... except
  from sheepdogguides.com*)

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls;

const ver='24Jly11';

type
  TDD96f1 = class(TForm)
    buQuit: TButton;
    laSum: TLabel;
    laAns: TLabel;
    Timer1: TTimer;
    procedure buQuitClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    bNum1,bNum2:byte;
    bAns:byte;
    procedure UpdateDisplay;
  public
    { Public declarations }
  end;

var
  DD96f1: TDD96f1;

implementation

{$R *.DFM}

procedure TDD96f1.buQuitClick(Sender: TObject);
begin
application.terminate;
end;

procedure TDD96f1.FormCreate(Sender: TObject);
begin
application.title:='DD96';//label for app's icon
DD96f1.caption:='DD96 '+ver;//text for title bar of window
bNum1:=250;
bNum2:=2;
UpdateDisplay;
end;

procedure TDD96f1.UpdateDisplay;
begin
bAns:=bNum1+bNum2;
laSum.caption:=inttostr(bNum1)+' plus '+inttostr(bNum2)+' equals...';
laAns.caption:=inttostr(bAns);
end;

end.

Fancier

Now we're going make the application fancier... but we won't yet (deliberately!) trigger any errors.

Put a "timer" component onto your form, if you haven't done so already, and double click on it to create a procedure to be executed each time the timer times out, vis...

procedure TDD96f1.Timer1Timer(Sender: TObject);
begin
bNum2:=random(3);
UpdateDisplay;
end;

.. and run that.

The code....

random(3)

... should generate a zero, a one, or a two. Never a three. That's just how the built-in random() function works.

Because our new code is in the handler for the Timer event, it will execute each time the timer times out. For the timer to time out, its "enabled" property must be set to "true"...which it will be, unless you fiddled with it. The timer will time out after 1000 milliseconds (aka one second) if the timer component's interval is 1000, which was the default value. (Set that to 500 now, to hasten the program's selection of new values.)

You may want to add a line to the FormCreate handler. If you add randomize; there, you won't get the same sequence of "random" numbers each time you run the program. I say you may want to add "randomize;" because sometimes a predictable sequence of numbers is better when you are debugging.

A detail: At the moment, our application always starts off with 250 plus 2. This is not so egregious that it is worth going into a "proper" fix for it. A simple, but crude, fix would be to replace the bNum2:=3; line with bNum2:=random(3);. That's a little crude, as you will probably change the code at the other place where bNum2 is chosen, and it would be more logical, tidy, elegant, to have the same rules used every time the number to add is chosen. And copying the code to both relevant spots each time you make a change to either is not the way to go... it would be too easy to forget to change the other one sometime. (The way to go, which I'm not going to illustrate, is to create a function... I'd probably call it "wPickNum2"... and call that function from the two places in the application that a new bNum2 is needed. All that to get around having the first one 3 every time? Too much hassle, for too little reward, I say.

Break it

So... we have a nice little program, showing 250 plus any one of several numbers. Change the line which chooses bNum2 to...

bNum2:=random(8);

... and two things happen...

With bNum2:=random(8);, you get 0,1,2,3,4,5,6 or 7 (not 8) in bNum2.

Run the program. Quite soon you will see something strange, if you watch carefully.

When the bNum2:=random(8); line chooses six or seven for the denominator, the answer to bNum1+bNum2 is "wrong". Well... wrong according to what we were taught when we were eight.

According to the computer....

This, according to the way the computer adds "byte" type numbers is right. It's a long story, connected to the fact that your car, if its odometer has only 5 digits, will appear to have gone 00045 miles if it has gone 100,045 miles.

250+6 may equal zero according to the rules of adding byte type numbers, but it won't be very helpful if you are writing a stock control application, and it says you have zero widgets in a bin which previously had 250 widgets to which you've added six more.

(By the way for novices... of course if you are writing a stock control application, and there may be more than 255 widgets in a given bin, all is not lost. Just use a different data type for the variable holding "how many widgets".)

Fix it

There's a simple thing you can do to help catch things like this. Despite my section title, it won't fix it... but identifying the bug is the first step along the road to fixing it.

Enable Delphi's range checking

Once you've enabled range checking, you will be alerted (in some instances) when your code violates the range boundaries of an object.

At the top of the implementation section of the program, you should find....

{$R *.DFM}

That's a compiler directive telling the compiler where to find resource files.

Add the following. Put it on a new line just after the {$R *.DFM}...

{$R+}

While it looks quite like the directive about where to find resource files, it has nothing to do with resource files. Instead, this directive turns on range checking. The Delphi help file discusses the pros and cons. I can't think of an application which I have written in which I haven't left range checking enabled.

Before you go any farther with this exercise, rearrange your windows so that the window with your code in it and the application's window do not overlap one another, and both are in the top 40% of your screen. A little crowded, perhaps, but the application window can be small. You'll see why I want things like this in a moment.

Change Timer1's interval property to 2500, and run the application again, after adding the {$R+}.

Before very long... as soon as random(8) returns a 6 or a 7... you should get a message box saying "Project DD96.exe raised and exception class ERangeCheckError... Process stopped... Use Step or Run to continue."

Click the "Ok" button to make the message go away, and you will find yourself looking at your code, with the bAns:=bNum1+bNum2; line highlighted. This is where Delphi detected an attempt to put something too big into the byte-type variable bAns.

If you run the application again, you won't be running it "from scratch". (I usually use the green triangle-arrow button to run my applications when I am working on their code, but you may be using F9 or the menu (Run | Run).)

As I was saying... you won't be running your application from scratch, you will be picking up from where the Delphi RAD system caught the fact that you were trying to do something odd.

(If you want to run it from scratch, use the menu: Run | Program Reset. That prepares the way to click the green "Run" arrow button for a "normal" (from scratch) "run"... but don't do that now.)

If you pick up from where Delphi suspended the code's execution, you will see your form again, with a simpler message box in the center of your screen just saying "Range Check Error". Leave it there, for the moment. Let Delphi continue executing your code.

Before very long, random(8) will return another six or another seven, and you'll get the long error message again, and the program's execution will be suspended again. Click the long message's "OK", tell Delphi to continue execution. There should now be two of the short message boxes near the center of the screen, slightly overlapping one another. If you are quick, you can now click each one's "OK" to make them go away. Before long the process will repeat.

The Delphi RAD system is running your code in a "debug" mode. Now that you've turned range check error trapping on (with the {$R+} directive), you will be alerted when you code is trying to do something which breaks the range checking rules. There will be two alerts for each breaking of the rule... one that you see because you are in the debug mode (the first one, the longwinded one), and one that any user would see (the short version).

Now... Make sure you're okay with what I've said so far, because I am now going to extend it.

So far, we've seen our code running under Delphi's supervision. Don't make any changes to it. Make sure it is NOT running at the moment, and not suspended awaiting your decision on "run from where we got to" or "reset the program and run from scratch". You don't need to close your Delphi session down, just make sure there are no error messages on the screen, and that there is no button for your application on the task bar.

In a moment, we're going to run the application "naked". We're going to run it outside the Delphi RAD system, to see what a normal user of your .exe file would see. After we start it running that way, it may be hard to click the error message "Ok"s fast enough to get rid of them all before new ones arise. You won't be able to hit the application's "Quit" button or use Alt-F4 to kill your application while there are error messages on the screen. (When you ran it under Delphi, the program was suspended each time a new error arose. Also, you could use program reset to (briefly!) get a "no errors" state, i.e. to clear accumulated error message windows.)

So! Having got DD96 stopped...

Use Windows Explorer to open a window showing the file DD96.exe. Double-click it, which will make it run "naked", i.e. make it run as a customer would see it. It will be running "outside" of the Delphi RAD environment.

Before long, you will get one of the short "Range Check Error" message boxes. Leave it on the screen. Look at the application's window. It is still running, right? So before long, you will get another "Range Check Error" message box. If you go away to make a now- long- overdue, well- deserved cup of coffee, by the time you get back, there will be a huge stack of "Range Check Error" message boxes.

While it is nice that your program doesn't die if it is, say, using additional hardware to monitor and record your local weather, and it is nice that you are getting alerts when your code is trying to do something weird....

Before we go on to the clever answer to these problems, I want to get something else out of the way. It will mean another artificial example. Sorry!

We're going to change our program slightly.

bNum1 will still start at 250, but it will go up by one each time the timer times out. And instead of displaying bNum1 plus a small random number each time the timer times out, we are going to calculate and display bNum1 minus 3.

To accomplish this, replace the earlier version of the following code with exactly what you see below... (If you happen to know about inc(), don't use it... yet!)

procedure TDD96f1.UpdateDisplay;
begin
bAns:=bNum1-bNum2;
laSum.caption:=inttostr(bNum1)+' minus '+inttostr(bNum2)+' equals...';
laAns.caption:=inttostr(bAns);
end;

procedure TDD96f1.Timer1Timer(Sender: TObject);
begin
bNum1:=bNum1+1;
UpdateDisplay;
end;

Once again, if you run the code from within the Delphi RAD, before long you will get the long message about a range check problem. It will happen when you try to add 1 to 255 in bNum1. (bNum1 being a byte-type variable can only hold integers in the range 0-255 (inclusive).)

To get out of the error state now, you will have to use the menu Run | Program Reset provision. You won't be able to click DD96's "Quit" button fast enough.

For reasons which entirely escape me, you will not, now, get the short "Range Check Error" message when you are running DD96 under the Delphi RAD. It will still appear when you run the application "naked", as a customer would use the .exe file. (If someone can send me an explanation of this, I would welcome the help!)

(Feel free to change the bNum1:=250 in the FormCreate handler to bNum1:=253, by the way, just to speed up the arrival of the error condition.)

A little "fine point"....

Now change the bNum1:=bNum1+1; line to inc(bNum1);. It will do the same thing to the contents of bNum1. And run the application again.

Same as before, yes? No!

When we had bNum1:=bNum1+1;, the application was stopped at that line, with a range check error. The Inc() procedure doesn't do a range check. The application still stopped with a range check error message, but it was at the bAns:=bNum1-bNum2; line. In this application, the difference wouldn't be terribly important. In other uses of Inc(), the problem might not be trapped subsequently quite as quickly, and the code would probably be more extensive... making it hard to find the problem. I can't "fix" this for you. All I can do is warn you that Inc() is fast, which you may need sometimes, but "weak" in that it doesn't check to see that ranges are not being violated.

Another thing...

While I don't want to depress you, and I certainly am not complaining about the brilliant Delphi which I have used very happily for many years... well more than a decade... I have to admit that in my current "to do" pile is the task of resolving why one of my programs is throwing up a range check error from time to time, and why, in that case, the program grinds to a halt when the range check error arises, instead of going on, perhaps generating more error messages, but at least going on, as our little demo program will go on. Ah well.. if it was easy...?

Finally! Try this! Getting fancy....

Whatever the solution of my problems (well, programming problems, anyway) may be, here's something neat you can do in programs when errors arise.

Before I tell you that, a quick word about "errors".

In our example, there's a fundamental flaw in the design of the program. You would not fix an error like that with clever error handling, but with a revision of the code to make the error stop appearing.

In the real world, though, you may well have to put "error" handling code into a program. Suppose, for instance, your application asks users to supply phone numbers. And suppose users sometimes enter letters instead of digits. It's been a long time since "Garden 8-5927", or "Sherborne 3449" were legitimate telephone "numbers". (Both once were, in my (long) lifetime! It was hard to dial them by candle-light, though.)

For the phone numbers problem, you would put error handling in around the part of the code where the users were entering phone numbers. Maybe by the techniques here, maybe by other techniques, but you'd need to do something if you wanted your application to be robust.

So! What's this super-duper error handling technique I wanted you to know?

Go back to the first version of DD96. You only need to restore....

procedure TDD96f1.UpdateDisplay;
begin
bAns:=bNum1+bNum2;
laSum.caption:=inttostr(bNum1)+' plus '+inttostr(bNum2)+' equals...';
laAns.caption:=inttostr(bAns);
end;

procedure TDD96f1.Timer1Timer(Sender: TObject);
begin
bNum2:=random(8);
UpdateDisplay;
end;

Change the timer's interval property back to 500, too. Make sure that's running as before.

Now change the UpdateDisplay procedure, making it....

procedure TDD96f1.UpdateDisplay;
begin

try
   bAns:=bNum1+bNum2;//Was in previous version
except
   on E : Exception do
     begin
       ShowMessage('Exception class name = '+E.ClassName);
       ShowMessage('Exception message = '+E.Message);
       bNum2:=0;//<< Don't miss this and the next line!
       bAns:=bNum1+bNum2;//Recalculate bAns
     end;
   end;//of "try... except..."

//same as before from here....
laSum.caption:=inttostr(bNum1)+' plus '+inttostr(bNum2)+' equals...';
laAns.caption:=inttostr(bAns);
end;

(My thanks to....

http://www.delphibasics.co.uk/Article.asp?Name=Exceptions

... for an essential element in that)

That's "overkill", and far from perfect so far... but it hints at the way to do many marvelous things.

Notice first the bNum2:=0;. If the random number generator came up with something which would result in a too-big bAns, during the error handling, the code says "no, don't use that. Use zero instead.".

(Of course, if you really wanted to add any of the sensible numbers to bNum1, and you had bNum1 at the original 250, you'd only need to make the bNum2:=random(8); into bNum2:=random(6);... but I did warn you that this example of using error handling would be a little artificial!)

Run the application again. It now works better. (Don't be puzzled by the times that you see 250+0=250 and no error message. They not only arise when a 6 or 7 has been changed to a zero, but also when random(8) has chosen a zero in the first place.)

If you are running the application under the Delphi RAD, you still need to acknowledge the occasional long range check error message, and tell Delphi to resume execution of the code. Also... something you may overlook... you will get a "pile" of "Exception class name =...." and "Exception message =..." message boxes on your screen if you don't keep clicking their "OK" buttons as the boxes arise. The short error messages are no longer being displayed in slightly offset positions. (You can drag the top one off of the pile, then the next one, and the next one... to see that what I say is true.)

Those error messages are "modal" (dreadful name)... which is an obscure way of saying that although the application will continue running even while they are on the screen, until they are cleared away (by clicking their "OK" buttons), you can't do anything else with the application, e.g. click its "Quit" button. The application is running, but you can't "switch focus" to the main form. (Through the use of a different way of presenting the messages, a way you would have to write the code for, you could make this "modal" problem go away.)

When you run the application "naked", it runs just fine. (Although the problem of the ever growing "pile" of messages remains.)

Those messages, done that way, at least, would not, I wouldn't think, often be part of a real world application. They were put in the demo code as the quickest way of showing you that E.ClassName and E.Message hold useful stuff. We will be returning to them.

The basic structure at the heart of this wonderful mechanism is....

TRY....
  something... and it can be many
  lines of code.
EXCEPT....
  and many lines of code can go here, too
END;

The end is the end of the try... except... block.

Don't (yet) worry about the "On E..." part of what we've done.

When the execution of the code reaches the spot marked by the word try, it passes on into the code below, but a "mental note" has been made.

If no "exceptions" (more on this in a moment) arise before execution reaches the except, the code execution then skips over everything between the except and the end which marks the end of the try... except... block.

If an exception arises, the execution of the code immediately skips to the except block.... any code as- yet- not- executed in the try part is skipped over.

To recap...

The code in the try part is executed up to the point that an exception arises. The except part holds the code to be executed after an exception has arisen... and only if an exception arises.

A "frill" before we go back into our code, and discuss some of the things we skipped past just now.

There's another structure for handling exceptions (we'll look at them properly in a minute)...

TRY....
  something... and it can be many
  lines of code.
FINALLY....
  and many lines of code can go here, too
END;

When code is written like this, execution "tries" to do what is in the try part. If an exception arises, execution jumps to the finally part. Even if no exception arises, the code in the finally block ALWAYS gets executed. If an exception arose while the try code was executing, the processing of that exception is deferred until what is in the finally part has been done.

It is similar to the try... except... structure this tutorial is about. You could say that try... except... and try... finally... are "cousins", a bit like repeat... until and while... do... are "cousins".

Good(?) news: I'm not going to go further with try... finally.... (Here... or, probably, even in another tutorial for some time.)

Right! Back to things we only touched on earlier....

Those exceptional exceptions

So.. we've looked a the try... except... structure. Now we will look at "exceptions", which is what the structure was made to handle.

First recall that Windows is a multi-tasking environment. While your code is executing, the obvious things which you mandated with your code are not the only things that are happening. While the code you are aware of creating is running, there may be other, less obvious, things going on, due to your application, "beneath the surface". The timer component is a perfect example. Putting a timer to work in your application is not difficult... and there is no need for you to think deeply about everything that is happening, but if you do think deeply for a moment now, you should realize that Delphi takes care of some neat things for you, "beneath the surface". The stuff in your timeout handler is "above" the surface, and you need to think about that every time. But how often have you thought about the fact that between timeouts, the timer component is still "doing things" while your application is off doing the things you will always be aware of? (The timer component if going tick, tick, tick, and watching for the right time to timeout.)

Back to "exceptions". By all means read the Delphi help file entry for "Exception". It left me with the impression exceptions are primarily used for error handling, which is the only use this tutorial delves into.

When some errors arise, an internal exception "message" is generated. With try... except... you can watch for those messages and deal with the error conditions when they arise.

Dealing with exceptions and dealing with events are similar things. For all I know one is a special case of the other. You don't work with each of them in exactly the same way, but I hope it helps you to hear that they are similar in some ways.

The code given in the example above is wonderfully but dangerously general. It catches any exception message, and applies a "one size fits all" answer to whatever exception has come along. The code also does something which puzzles me. We'll come back to these issues in a moment.

First, a recap of the Case.... structure, which does something similar to what happens in the except part of try... except.... If you are familiar with using case.. of.. , this may help you. If you aren't familiar with it, don't struggle with it now, just skim the next material.

Case of...

When I was learning to program in school back in the late 60's, a great favorite was a nick-name translator.

We used crude programming languages back then, and the program would look something like the following, assuming that Mr. Ranhoff's nickname was "Pooter", and Mr. Blythe's was "Goomy"

10  input n$
100 if n$="Pooter" then print "Mr. Ranhoff"
110 if n$="Goomy" then print "Mr. Blythe"
200 goto 10

There are all sorts of problems with that, but it will do to illustrate the idea of the translator program. And, crude though they were, the languages of those days were more beginner friendly! (No, I wasn't using BASIC in the late 60's, it was something on a PDP-8... but it was similar.

You could create an application which "translates" in Delphi, too. The following would only be part of the program, the part which would be executed when a name had been supplied to the edit box called eName. You'd probably fix things (with the OnKeypress event handler... or would it be the OnChange handler?) so that when a name had been entered and then the enter key pressed, the following would be executed. I've "cheated" a little bit, just to get to the example of using the on... structure...

bNameIndex:=255;
if eName.text='Pooter' then bNameIndex:=0;
if eName.text='Goomy' then bNameIndex:=1;
if eName.text='Romani' then bNameIndex:=2;
//That just sets the stage for a "case" statement...

case bNameIndex of
  0:laNicknameIs.caption:='Mr. Ranhoff';
  1:laNicknameIs.caption:='Mr. Blythe';
  2:laNicknameIs.caption:='Mr. Milnor';//; allowed here
  else begin
    laNicknameIs.caption:='Not known';
    end;//of else
end;//of case

Look at the part starting case bName...

When we get there, something is in bNameIndex. The program then works its way down through the lines 0:..., 1:... and 2:... (There could be many more.)

When it gets to the statement starting with the number that is in bNameIndex (followed by a colon), it does what it finds there. If it gets through all of the provided- for options without finding the number in bNameIndex, it executes what is in the "else" clause.

(Remember that in an if... then.... else... structure, you must not have a semicolon after the last statement before the else. The last (number)(colon)... line of a case... of... structure allows (requires?) one. The reason is simple, if you think about it. But... back to try... except...

The link between case... and try... except... is that, after the except you can have multiple potential courses of action. Each one will start with the word "on" (lacking in the case... structure). Each then has the circumstance in which you want the code that follows executed.

There is a colon (:) in the try... finally... example above, but it is not analogous to the colons in the case... structure.

Previously, we had code which, after some less important stuff is snipped away, is....

try
   bAns:=bNum1+bNum2;
except
   on E:Exception do
     begin
       ShowMessage('Exception class name = '+E.ClassName);
       ShowMessage('Exception message = '+E.Message);
       bNum2:=0;//<< Don't miss this and the next line!
       bAns:=bNum1+bNum2;//Recalculate bAns
     end;
   end;//of "try... except..."

laAns.caption:=inttostr(bAns);

Confession of a one eyed king: I don't fully understand the E:Exception bit.... but I can use it!

First, as I said a moment ago, do not confuse the colon (:) in that with the colons in the case... structure. Within the code above, the colon is used the way it is used in, say....

var b1, b2:byte;

I believe that the effect of the E:Exception bit is (sort of) to dynamically and temporarily create a variable called "E", of data type "Exception". Exactly what is going on is described in the Delphi help file entry for "Try... except..."... but I didn't find that I fully grasped what it was saying. But... as I said... I can use the E:Exception thing! And will show you how to use it.

"Exception" is the name of a class. There are many other classes derived from it. This is a matter of using the hierarchical features of Object Oriented Programming. It is a general thing, a bit like a stem cell. The others are more specific. Why you should grasp this will, I hope, become clear.

Don't, by the way, worry about destroying the thing called "E", created in the code above. It is destroyed for you at the right time. (This advice pp the Delphi help file entry "Using the exception instance".)

When we said....

except
   on E:Exception do
     begin...

We were saying, "whatever sort of exception has been raised... do this. And when we need information about the exception which was raised, look at, for instance, E.ClassName or E.Message... there will be information in those fields for you.

We can be a bit fancier when we have a idea of the sort of exception which is going to arise. (In our program, the exception we caused to arise was a Range Check Error exception, for instance.)

Here's a bit of code that uses a label on the form to display the most recent exception. The label is empty when we didn't go into the except... block on the most recent pass through the try... except... structure. As presented here (we will expand it shortly), the code only "notices" range errors.

It is easier, by the way, to test these illustrations by running the .exe file directly, "naked", by double-clicking on them in a Windows Explorer view, rather than running them inside the Delphi RAD. That can remain running while you test each variant of the program. (Be sure to re-compile the program before re-trying the .exe file!) (Running them "naked" spares you the extra error messages, the ones from the Delphi RAD debugging services, and it saves you restarting the application after each exception is noticed by the debugger.)

try
   bAns:=bNum1+bNum2;
   laErrMsg.caption:='';
except
   on ERangeError do begin
       laErrMsg.caption:='Error: valid range exceeded detected.';
       bNum2:=0;
       bAns:=bNum1+bNum2;
       end;//End of processing ERangeError

The try... except... structure allows an else... clause... but I couldn't get it to do all I wanted. Instead, I think what you want is....

try
   bAns:=bNum1+bNum2;//Was in previous version
   laErrMsg.caption:='';
except
   on ERangeError do begin
       laErrMsg.caption:='Error: valid range exceeded detected.';
       bNum2:=0;
       bAns:=bNum1+bNum2;
       end;//; allowed here. End of processing ERangeError
   on E:Exception do begin// will only execute if exception not yet handled.
       laErrMsg.caption:=E.ClassName;
       end;// of on E:Exception do begin

In that, the code first checks if the exception message was raised by a range check problem... in which case, the first block of code will be executed, and the rest of the on... blocks will be skipped over.< (There may be several on... blocks before the final one.)

The final on... block serves as an "else" handler, because I used the E.Exception thing, the think that catches any exception not already dealt with. Thank heavens for whatever mechanism causes the later on... blocks to be skipped.... I couldn't find how to "load" E with the information about the exception message prior to looking at the different cases which might need handling.

By the way... there is nothing to stop you building the following, if you think your code, at this point, may sometimes give rise to an range check error OR a divide by zero error (not that I can see how one would arise following our other code...

try
   bAns:=bNum1+bNum2;//Was in previous version
   laErrMsg.caption:='';
except
   on ERangeError do begin
       laErrMsg.caption:='Error: valid range exceeded detected.';
       bNum2:=0;
       bAns:=bNum1+bNum2;
       end;//of dealing with ERangeError
   on E:EDivByZero do begin
       laErrMsg.caption:='Error: divide by zero attempt detected.'+E.Message;
       //Do something about it
       end;//of dealing with EDivByZero
   on E:Exception do begin// will only execute if exception not yet handled.
       laErrMsg.caption:=E.ClassName;
       end;//of "on E:Exception.." (Our "else")

That expanded example is there to illustrate....

* There can be more than 2 on clauses, and
* You can access the contents of the exception message via the "E:" mechanism in more than one of the alternative on... clauses.

You don't have to use the name "E" for where you store the information from the exception message... but I can think of no reason not to. The storage is transitory anyway. You probably need to not have anything else called "E" in your application, if you do use "E" here.

You do have to use "ERangeError", and "EDivByZero" to "catch" those exception messages, and all the other possible exceptions also have their own fixed names. (The easy way to discover them is by putting the "catch all" on E:Exception... at the bottom of every except... section. Gradually you will learn the names of the exceptions your programming mistakes give rise to!

A topic for the future: You can create your own exception messages and cause them to arise. I'll leave that advanced activity to your own researches. The Delphi help file... have I mentioned it?... has information for you... have a look at the "Raising an exception" as one starting point, if you want to explore making your own exceptions, raising them, handling them, etc. (Throughout this essay, the help file I am referring to is the one which came with Delphi 4, which is the version of Delphi all of this has been tested under.)

What to do with messages about exceptions

In our exploration of handling exception messages, we've been content (briefly!) to use the showmessage() procedure.

We moved on from that to display information about exception events in labels on the form, to get away from the need to dismiss the showmessage dialogs.

It is beyond the scope of this tutorial, but hardly rocket science, to have messages about exception events written to an error log data file, and this is probably the Right Way To Go for programs which are doing serious work, or even being sold to customers.

Nothing "special" is needed, although of course some care will be needed to ensure the file is open, "ready for business" before the first exceptions arise, and that the closing of the file is done "nicely" along the way of every potential exit from the application.

Where were we when the error arose??

If you implement a system to record messages about exception events in a data file, the information will be more helpful if you know where the program was in the code at the time the exception occured.

For this you can use a semaphore. Set up a global variable; I'm calling mine sSemaphore.

From time to time, at strategic points in the code, change the contents of sSemaphore.

And write the current contents of sSemaphore to the error log whenever you record something. From what was in sSemaphore, you will be able to deduce something about what was executing when the exception was raised. And as your work with the application progresses, it will be easy to add more sSemaphore-changing lines as needed to narrow down where common errors arise.

Now... I said "Change the contents of sSemaphore". You could, maybe, just use a number. "1" at the beginning, "2" at a later point, "3" at a still later point. If you can stay on top of such a scheme, my hat is off to you. I'd rather do the following....

If I were to be adding another "change what is in sSemaphore" line to a program at 2:17pm on 23 May 2011, I might use....

sSemaphore:='2011-05-23/14:17';

By basing my sSemaphore text on the date and time it was added, I can be sure of not having two places in the code where I've said...

sSemaphore:='5';

You can, of course, "compact" things like "2011-05-23/14:17", if you are worried about the size the error log data file might grow to.

You might shy from such a scheme on the basis of it lacking "organization". You don't need to be organized about any correspondence between the contents of the semaphore and where the contents were changed. If you are looking for where the semaphore became '2011-05-23/14:17', you can just use the code editor's "Find" function! (You do need to be careful not to duplicate the semaphore values... hence the section of this tutorial you are reading!)

Just as an illustration of compressing date/time semaphores... you can devise your own scheme... I would be inclined to put something like the following in the comments at the head of the application's code...

sSemaphore will hold a value which
  is there to help you identify where
  things happened during the code's
  execution.

The contents of sSemaphore will indicate
  when that sSemaphore-changing line was
  first added to the application's code.
  They will be the following form...

ymdhhmm

... where...

y gives the year, "a" for 2011, "b" for 2012, etc.
m gives the month: 1,2,3... for Jan, Feb, Mar,
    and a, b, c to indicate Oct, Nov and Dec
d gives the day: a for 1, b for 2... z for 26,
    A for 27, B for 28, etc. (Case significant)
hh gives hour, with leading zeros employed,
  24 hour clock
mm gives minutes, with leading zeros employed.

Examples:

a5w1417 for 2011-05-23/14:17
b1a0115 for 2012-01-01/01:15
bca1345 for 2012-Dec-01, 1:45pm
Etc.

(You could "easily" save another digit, if you are feeling manic... I'll leave how as a little puzzle to solve. Nothing fancy. There are, of course, even more compact schemes.)

Divide and conquer... just don't divide by zero

Let's look at another error which arises from time to time... the "divide by zero" error, and let's look at how Delphi reacts to it....

In some circumstances (I will explain) Delphi will tell you that the answer to "a number divided by zero" comes to infinity. Well. Sort of. Not really, though.

You cannot divide anything by zero. It breaks the rules. In particular it breaks the rule illustrated by the following...

If 50 divided by 10 equals 5, then
it is also true that
10 times 5 equals 50.

Thus, if 50 divided by zero equals infinity,
then zero times infinity should equal 50,
and it doesn't.

Hence the rule that you shouldn't try to divide by zero.

But programmers often ask the poor computer to do just that.

And an exception is raised, and if you've put exception handling into your code, you will be told about the problem.

In Delphi, there are two sorts of "dividing". To do the one most familiar to any numerate person, the answer to the exercise must be sent to a variable of one of the "real" types, e.g. "single". The following will work fine...

var:bTmp0,bTmp1:byte;
    siAns:single;

....

bTmp0:=1;
bTmp1:=3;
siAns:=bTmp0/bTmp1;

The following will just refuse to compile...

var:bTmp0,bTmp1:byte;
    siAns:single;

....

bTmp0:=1;
siAns:=bTmp0/0;

.. but the following, nearly the same, will compile...

var:bTmp0,bTmp1:byte;
    siAns:single;

....

bTmp0:=1;
bTmp1:=0;
siAns:=bTmp0/bTmp1;

... and then when you try to run it, a "EZeroDivide" exception will pop up... sometimes...

If you add showmessge(floattostr(siAns)) after the above, you will get "INF", as in "infinity". That's what I meant about Delphi giving you that answer for a divide by zero.

An aside for keen students and experts....

Here is a version of the try... except... code we have been working with. When the following is run, "naked", i.e. by double clicking on the .exe, the "EZeroDivide" message should, I believe, appear in the laErrMsg label each time the code tries to divide by zero. Why is it only appearing once? (I don't know why. That isn't a Socratic question. I really want to know!)

unit DD96u1;

(*Illustration of using "try... except.."
 This is one of several versions of DD96. See Delphi
 tutorials at sheepdogguides.com*)

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls;

type
  TDD96f1 = class(TForm)
    buQuit: TButton;
    laSum: TLabel;
    laAns: TLabel;
    Timer1: TTimer;
    laErrMsg: TLabel;
    procedure buQuitClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
    bNum1,bNum2,bAns:byte;
    procedure UpdateDisplay;
  public
    { Public declarations }
  end;

var
  DD96f1: TDD96f1;

implementation

{$R *.DFM}
{$R+}

procedure TDD96f1.buQuitClick(Sender: TObject);
begin
application.terminate;
end;

procedure TDD96f1.FormCreate(Sender: TObject);
begin
randomize;
bNum1:=250;
bNum2:=0;
UpdateDisplay;
end;

procedure TDD96f1.UpdateDisplay;
var siAns:single;

begin
try
   siAns:=bNum1 / bNum2;
   laErrMsg.caption:='';
except
   on ERangeError do begin
       laErrMsg.caption:='Error: valid range exceeded detected.';
       bNum2:=0;
       bAns:=bNum1+bNum2;
       end;//; allowed here. End of processing ERangeError
   on E:Exception do begin// will only execute if exception not yet handled.
       laErrMsg.caption:=E.ClassName;
       end;// of on E:Exception do begin
   end;//of "try... except..."

laSum.caption:=inttostr(bNum1)+' / '+inttostr(bNum2)+' equals...';
laAns.caption:=floattostr(siAns);
end;

procedure TDD96f1.Timer1Timer(Sender: TObject);
begin
bNum1:=random(8);
if bNum1<3 then bNum2:=2 else bNum2:=0;
UpdateDisplay;
end;

end.

Right... back to telling you things rather than asking you things.

So far so good? Apart from the little mystery of the missing messages?

In Delphi, and other decent languages, there's another sort of "dividing" which you can do. It is in the div function, which works like this...

 6 div 3 is 2
 7 div 3 is 2
 8 div 3 is 2
 9 div 3 is 3
10 div 3 is 3
11 div 3 is 3
12 div 3 is 4
13 div 3 is 4

... and so on.

But you can't do, say, 6 div 0. If you were to do...

var:bTmp0,bTmp1,bAns:byte;
....

bTmp0:=6;
bTmp1:=0;
bAns:=bTmp0 div bTmp1;

... then the exception raised would be "EDivByZero".

Note that it is different from the exception arising from trying to do a "/" by 0 ("EZeroDivide"). No big deal. I just thought I'd tell you to watch out for the subtle difference there.

Enough dividing.

That's it for now

That's about it for this tutorial. Feedback is always appreciated, especially if you tell me what page you are writing to me about.

Do you use try... except... already? What do you find it good for? What in the above didn't make sense to you?

Remember the "cousin" of try... except... which I mentioned briefly, try... finally.... Is that one you like?

Any other exceptions which you use frequently?





            powered by FreeFind
  Site search Web search
Site Map    What's New    Search This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"


Click here if you're feeling kind! (Promotes my site via "Top100Borland")

If you visit 1&1's site from here, it helps me. They host my website, and I wouldn't put this link up for them if I wasn't happy with their service. They offer things for the beginner and the corporation.www.1and1.com icon

Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Tutorials main page
How to contact the editor of this page, Tom Boyd


Valid HTML 4.01 Transitional Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....