Using try..except to handle errors

Learn about exceptions and how to write better error handling code. Learn how Castalia can help us write more robust and fault tolerant code.

(Originally published at delphi-expert.com on March 23, 2004)


As promised, today is part two of our discussion of Delphi's try constructs. Last week, we looked at try..finally, learned what it is intended for, when to use it, and how Castalia can help us use it more effectively. This week, we will examine the other half of the try family, try..except and learn how Castalia can make our try..except usage more effective. We will also make fun of java.

Try..except is a construct for handling "exceptional" situations in a program. A program has a typical flow, which is a set of instructions that proceed in order. Sometimes this order involves branches, but there is always a logical flow, and decisions to branch that flow are based on some state of the program, like whether X is less than 5.

Sometimes, however, that state can be rendered incorrect. Let's take an example that we might see in the real world:

AssignFile(F, 'SomeFile.Txt');

ResetFile(F);

S := ReadLn(F);

CloseFile(F);

This simple code will open a file, read a line from it, and close it again. This is the normal flow of the program. However, a number of assumptions are made about this flow. First, we assume that the file exists. We assume that we can open it for reading. We assume that there is a line to be read. Etc...

If any of these assumptions turns out to be false, then we have an exceptional situation! The flow of the program is going to be messed up by the fact that some state is not the state we expected.

There are several ways to handle these exceptional situations. We could ignore them, and if any of them occurs, the program will crash or do something else equally erratic. We could check for those exceptions, adding code like this:

if not FileExists('SomeFile.txt') then

begin

WriteLn('File does not exist');

Exit;

end;

This is better. For one thing, you have actually told the user why the program won't work. For simple programs, this works well, but as the program becomes more and more complex, the state of the program will become exponentially complex, and soon you will be unable to check all of the assumptions. Checking your assumptions is a good thing though. We'll come back to this later.

We can also checkafter each operation to make sure that what we expected to happen. This is the traditional C approach, and the approach of the Windows API (and most other operating system APIs, with BeOS as one notable exception). In this model, every function returns an error code. It is your responsibility to check the error code and see if it is "OK" or something else like FILE_NOT_FOUND. Then, once again, you would inform the user and exit.

This is, in the end, a variation on the same approach, only this time we're checking the assumptions after we do something, rather than before. The advantage is that the each function can check its own assumptions, and in the case of the Windows API, all possible return values are pretty well documented, so with disciplined writing, it is actually possible to handle every condition, assuming that whoever wrote the API took all of his assumptions into consideration. Code written with this approach would look something like this (No, this is not valid pascal, my apologies to the purists):

ErrCode := AssignFile(F, 'SomeFile.Txt');

if ErrCode = FILE_NOT_FOUND then

begin

WriteLn('File Not Found');

Exit;

end;

ErrCode := ResetFile(F);

if ErrCode = FILE_NO_ACCESS then

begin

WriteLn('You don''t have access to do that.');

Exit;

end else

if ErrCode = FILE_ALREADY_OPEN then

begin

WriteLn('Someone else is reading the file');

Exit;

end;

ErrCode := ReadLn(F, S); //Let's assume this means read from F into S

if ErrCode = NOT_ENOUGH_TEXT then

begin

WriteLn('Not Enough Text');

Exit;

end;

CloseFile(F);

You get the idea. It could be much worse, but our program has gotten really complex really quickly, and all to accommodate things that don't normally happen.

So, someone in the last couple decades had a new idea. Why not create a control structure specifically for exceptional situations? This new could control structure would allow us to define what should happen normally, and then define what should happen if something goes wrong. This is known as "exception handling."

The basic idea behind an exception is that we assume that the normal flow control is going to take place. If it doesn't, we instantly stop execution wherever we are and exit the current function. We then exit the function that called that function, and so on, until, we reach an exception handler, which is a block of code that is executed only when an exceptional state or event takes place. If the normal flow of execution is followed, exception handlers are ignored.

In Delphi, the normal flow of execution is in a try block, and the exception handler is in an except block. Here's a simple example:

try

AssignFIle(F, 'SomeFile.txt');

ResetFile(F);

S := Readln(F);

CloseFile(F);

except

WriteLn('An error occurred');

end;

This is cleaner, and it allows us to clearly see what code we expect to be executed normally, and what code should be executed when something weird happens (not like that last example... shudder).

Of course, it would be nice to be able to tell the user exactly what error occurred. Exception handling makes this a breeze too. Since exception handling developed hand-in-hand with object-oriented programming, things called exception objects were conceived. The exception object is automatically created when an exception is raised. If we want to use the exception object, we extend the try..except syntax like this:

try

AssignFile(F, 'SomeFile.txt');

ResetFile(F);

S := Readln(F);

CloseFile(F);

except

on E: Exception do

WriteLn(E.Message);

end;

You see, each exception has a Message property that gives some short useful information about the specific exception. It might contain the string "File Not Found," or "File already in use," or something else that we would like to tell the user.

There are different types of exceptions, and we can take advantage of that too. Perhaps we want to do something special when a certain exception happens. Maybe we want to beep if the file is not found:

try

AssignFile(F, 'SomeFile.txt');

ResetFile(F);

S := REadln(F);

CloseFIle(F);

except

on E: FileNotFound do

Beep;

on E: Exception do

WriteLn(E.Message);

end;

Note that when we handle multiple exceptions, the most specific exception must go first. This is because exceptions are polymorphic - that is, Exception is EVERY exception, and EFileNotFound is an Exception, so if Exception is handled first, EFileNotFound will never be handled.

One problem with this mechanism is that we're still not sure what exceptions a particular function or procedure might raise. Some languages or development tools have a mechanism to help you know what might be raised. One C# IDE, for instance, will give you a small hint window to tell you what exceptions you should handle whenever you use a function. Java is notorious (and hated by many) for the fact that it enforces exception handling with terrorist zeal. Your code won't even compile if you don't handle the exceptions that it wants you to, even if you KNOW that those exceptions aren't going to happen, either because you have control over the running environment, or your code has already prechecked the condition and ensured that the exception can't possibly be thrown. Java will enforce its code style to the last jot and tittle, whether you like it or not.

Unfortunatly, no such tool exists for Delphi. As I write this, I'm beginning to think it might be a good thing to add to Castalia. I'll have to look into that.

Whew!

That was three and a half pages of Microsoft Word, just to understand what an exception is. Now for the fun stuff:

Castalia can help you write both your normal code and your exception handling code. Castalia's code templates define a trye template that automatically creates a try..except block for you and sets up entry points so that you can properly create your code. This will save you more keystrokes and more time than you realize!

Castalia's refactoring tools provide an automated surround with... refactoring that will put a try..except block around your code. Just highlight the normal flow control code, right click on the editor (or press Shift+Ctrl+R), and choose the appropriate item from the refactoring menu. The code will be changed automatically.

This week, we have learned what the try..except block is for and where it came from (and why it's the best error handling mechanism yet). We have also learned how Castalia can make your use of this powerful control structure to write more robust and user-friendly code.

For more information about Castalia's code templates, click here.

For more information about Castalia's automated refactorings, click here

To buy Castalia right now because this article was the best thing you have ever read, click here, then click here to read a much better book.

 

Share this article!

Follow us!

Find more helpful articles: