Exceptional exceptions

By: Marcelo Lopez Ruiz

Abstract: Learn about structured exceptions and some unusual ways to use them. By Marcelo Lopez Ruiz.

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.

Server Response from: SC4