This article gives some information on structured
exceptions in Delphi, and shows some interesting cases.
Normal Exception Usage
There are two kinds of exceptions: try..finally blocks, and
try..except blocks. Typically, you use try..finally blocks
to protect resources, and try..except blocks to handle
exceptions.
The following two examples illustrate these uses:
procedure TForm1.Button1Click(Sender: TObject);
var
Strings: TStringList;
begin
Strings := TStringList.Create;
try
Strings.Add('Hello, world!');
Strings.SaveToFile(ChangeFileExt(ParamStr(0), '.txt'));
finally
Strings.Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
Strings: TStringList;
begin
Strings := TStringList.Create;
try
Strings.Add('Hello, world!');
try
Strings.SaveToFile(ChangeFileExt(ParamStr(0), '.txt'));
except
on E: Exception do begin
ShowMessage('The following string could not be saved:'#13#10 +
String.Text);
end;
end;
finally
Strings.Free;
end;
end;
The first example shows a normal use of a try..finally block. We
create a TStringList object, and because we have acquired a resource
(memory, in this case), we want to make sure that it's released.
There are many things that could fail in the rest of the code -
maybe there's not enough memory to create a new string, or we
don't have permission to write to the new file. Or we exceed our
disk quota. We don't know all the possible causes of failure, but
we do know we want the memory to be released. try..finally allows
us to provide this behaviour with small fuss.
The second example shows a normal use of a try..except block. We
can't save the string to a file, so we change our behaviour and
show the string to the user. Note that the try..except block is
embedded in the try..finally block - in this case, we still want
the memory to be released.
Exceptional Exceptions
Now I'll show you some other examples of code, which highlight some
interesting uses of exceptions.
Creation Routines
function CreateStringsFromFile(const FileName: string): TStringList;
begin
Result := TStringList.Create;
try
Result.LoadFromFile(FileName);
except
Result.Free;
raise;
end;
end;
This first example creates a TStringList, loaded with the contents
of a file. This routine is meant to be a replacement for TStringList.Create.
Because of this, we want to make sure we keep the same allocation semantics.
Let me elaborate on this.
When we call a constructor, we are assured that either the call succeeds
and we get a valid object, or the call fails and all resources are released.
This is why we can place a constructor right before a try..finally block -
if the constructor raises an exception, there will be nothing to free.
We must keep the same semantics for CreateStringsFromFile. If we raise
an exception, we want to make sure that there's nothing left to release.
If we return correctly, then we have a valid object.
In this case, we accomplish this by using a try..except block. First, we
create the object; if this fails, we don't handle the exception, but we
let bubble up. If we get the memory successfully, then we will load the
contents from a file. Here, there are a million things that could go wrong -
if an exception is thrown, we will release the memory we allocated (because
we will not be returning a valid object), and allow the exception to bubble
up - in case our caller can deal with it intelligently, such as prompting
the user for another file and retrying.
Generic Creation Routine
Trying to understand all this, remember this little rule: for routines
which will create and return an initialized object, make sure you sandwich
all the code inside a try..except block.
function CreateWhatever: TWhatever;
begin
Result := TWhatever.Create;
try
InitializeWhatever;
except
Result.Free;
raise;
end;
end;
Freedom!
Every class has a constructor, which is used to create instances
of that class. The constructor is invoked on the class itself, not
on the object, which is why we write
Strings := TStringList.Create;
and not
Strings := Strings.Create;
or
Strings.Create;
The Free method is a very special one: even if you
invoke it on an instance which is set to nil, it will not generate
an exception. So the following code is quite ok:
Strings := nil;
Strings.Free;
This is an important thing to remember when reading the next
section.
To Protect The Many
This next example shows how to protect many objects in with nested
exception blocks, and how to do the same thing in one try..finally
block.
procedure TForm1.Button5Click(Sender: TObject);
var
Strings1: TStringList;
Strings2: TStringList;
Strings3: TStringList;
begin
Strings1 := TStringList.Create;
try
Strings2 := TStringList.Create;
try
Strings3 := TStringList.Create;
try
Strings1.Add('Hello, world!');
Strings1.SaveToFile(ChangeFileExt(ParamStr(0), '.txt'));
{ This will raise an exception, but all resources will be freed correctly. }
Strings2.Delete(51);
finally
Strings3.Free;
end;
finally
Strings2.Free;
end;
finally
Strings1.Free;
end;
end;
procedure TForm1.Button4Click(Sender: TObject);
var
Strings1: TStringList;
Strings2: TStringList;
Strings3: TStringList;
begin
Strings2 := nil;
Strings3 := nil;
Strings1 := TStringList.Create;
try
Strings2 := TStringList.Create;
Strings3 := TStringList.Create;
Strings1.Add('Hello, world!');
Strings1.SaveToFile(ChangeFileExt(ParamStr(0), '.txt'));
{ This will raise an exception, but all resources will be freed correctly. }
Strings2.Delete(51);
finally
Strings1.Free;
Strings2.Free;
Strings3.Free;
end;
end;
As you can see, the second example is shorter and clearer. However,
we have to make sure that we can process the finally block
correctly even if it is executed before all objects are created.
Therefore, we make sure that all pointers are assigned some value (the
compiler will generate a warning if you don't do it like this).
Remember that a call to Free will do nothing if the
object has a nil value, so it's safe to call it even if
the object hasn't been constructed.
I want out!
The next example will show you how to release a resource
before waiting for the finally block to execute.
procedure TForm1.Button4Click(Sender: TObject);
var
Strings1: TStringList;
Strings2: TStringList;
begin
Strings2 := nil;
Strings1 := TStringList.Create;
try
Strings2 := TStringList.Create;
Strings1.Add('Hello, world!');
Strings1.SaveToFile(ChangeFileExt(ParamStr(0), '.txt'));
FreeAndNil(Strings1);
{ This will raise an exception, but all resources will be freed correctly. }
Strings2.Delete(51);
finally
Strings1.Free;
Strings2.Free;
end;
end;
Suppose that Strings1 was using a lot of resources,
and we needn't keep it alive until the finally block was
executed. We can free it in the middle of the protected
code, but we need to make sure that the variable is set
to nil - otherwise, the program will generate an access
violation when trying to free the object twice. This is the same
case as in the previous example - we make sure that we handle
uninitialized resources correctly in the finally block.
FreeAndNil, for Delphi 5 users, performs exactly this function.
Users of previous Delphi versions can write their own routine or
simply inline the code:
Strings1.Free;
Strings1 := nil;
Well, I hope I've given you some food for though on how to use
exceptions (we haven't touched to topic of how to use
them to ensure transaction integrity, perform rollbacks or
use Abort for user cancellations). Using exceptions correctly
will make your program more user-friendly, much more stable, much
more professional, and it will give you a warm, fuzzy,
I'm-in-control-and-I-know-what-I'm-doing feeling.
So, how did you like this article? I'm very interested in your opinion -
you can add corrections, criticism, and requests for further topics
with the Add comment link at the bottom of this page.
Marcelo Lopez Ruiz has toyed with computers and programming since he was
old enough to read. He fell for with Delphi not long
after the first version was released, and while he works
with many other languages and tools, he remains true to his first love. He has
published a number of CodeCentral submissions and is the original
author of the MS SQL/Access To InterBase Wizard migration tool.
He can always be reached at
marcelo.lopezruiz@xlnet.com.ar.