Another key feature of Delphi is its support for exceptions. Exceptions make programs more robust by providing a standard way for notifying and handling errors and unexpected conditions. Exceptions make programs easier to write, read, and debug because they allow you to separate the error-handling code from your normal code, instead of intertwining the two. Enforcing a logical split between code and error handling and branching to the error handler automatically makes the actual logic cleaner and clearer. You end up writing code that is more compact and less cluttered by maintenance chores unrelated to the actual programming objective.
At run time, Delphi libraries raise exceptions when something goes wrong (in the run-time code, in a component, or in the operating system). From the point in the code at which it is raised, the exception is passed to its calling code, and so on. Ultimately, if no part of your code handles the exception, the VCL handles it, by displaying a standard error message and then trying to continue the program by handling the next system message or user request.
The whole mechanism is based on four keywords:
try Delimits the beginning of a protected block of code.
except Delimits the end of a protected block of code and introduces the exception-handling statements.
finally Specifies blocks of code that must always be executed, even when exceptions occur. This block is generally used to perform cleanup operations that should always be executed, such as closing files or database tables, freeing objects, and releasing memory and other resources acquired in the same program block.
raise Generates an exception. Most exceptions you'll encounter in your Delphi programming will be generated by the system, but you can also raise exceptions in your own code when it discovers invalid or inconsistent data at run time. The raise keyword can also be used inside a handler to re-raise an exception; that is, to propagate it to the next handler.
Exception handling is no substitute for proper control flow within a program. Keep using if statements to test user input and other foreseeable error conditions. You should use exceptions only for abnormal or unexpected situations.
The power of exceptions in Delphi relates to the fact that they are "passed" from a routine or method to the caller, up to a global handler (if the program provides one, as Delphi applications generally do), instead of following the standard execution path of the program. So the real problem you might have is not how to stop an exception but how to execute code even if an exception is raised.
Consider this code, which performs some time-consuming operations and uses the hourglass cursor to show the user that it's doing something:
Screen.Cursor := crHourglass; // long algorithm... Screen.Cursor := crDefault;
In case there is an error in the algorithm (as I've included on purpose in the TryFinally example's event handlers), the program will break, but it won't reset the default cursor. This is what a try/finally block is for:
Screen.Cursor := crHourglass; try // long algorithm... finally Screen.Cursor := crDefault; end;
When the program executes this function, it always resets the cursor, regardless of whether an exception (of any sort) occurs.
This code doesn't handle the exception; it merely makes the program robust in case an exception is raised. A try block can be followed by either an except or a finally statement, but not both of them at the same time; so, if you want to also handle the exception, the typical solution is to use two nested try blocks. You associate the internal block with a finally statement and the external block with an except statement, or vice versa as the situation requires. Here is the skeleton of the code for the third button in the TryFinally example:
Screen.Cursor := crHourglass; try try // long algorithm... finally Screen.Cursor := crDefault; end; except on E: EDivByZero do ... end;
Every time you have some finalization code at the end of a method, you should place the code in a finally block. You should always, invariably, and continuously (how can I stress this more?) protect your code with finally statements, to avoid resource or memory leaks in case an exception is raised.
Handling the exception is generally much less important than using finally blocks, because Delphi can survive most exceptions. Too many exception-handling blocks in your code probably indicate errors in the program flow and possibly a misunderstanding of the role of exceptions in the language. In the examples in the rest of the book, you'll see many try/finally blocks, a few raise statements, and almost no try/except blocks.
In the exception-handling statements shown earlier, you caught the EDivByZero exception, which is defined by Delphi's RTL. Other such exceptions refer to run-time problems (such as a wrong dynamic cast), Windows resource problems (such as out-of-memory errors), or component errors (such as a wrong index). Programmers can also define their own exceptions; you can create a new inherited class of the default exception class or one of its inherited classes:
type EArrayFull = class (Exception);
When you add a new element to an array that is already full (probably because of an error in the logic of the program), you can raise the corresponding exception by creating an object of this class:
if MyArray.Full then raise EArrayFull.Create ('Array full');
This Create constructor (inherited from the Exception class) has a string parameter to describe the exception to the user. You don't need to worry about destroying the object you have created for the exception, because it will be deleted automatically by the exception-handler mechanism.
The code presented in the previous excerpts is part of a sample program called Exception1. Some of the routines have been slightly modified, as in the following DivideTwicePlusOne function:
function DivideTwicePlusOne (A, B: Integer): Integer; begin try // error if B equals 0 Result := A div B; // do something else... skip if exception is raised Result := Result div B; Result := Result + 1; except on EDivByZero do begin Result := 0; MessageDlg ('Divide by zero corrected.', mtError, [mbOK], 0); end; on E: Exception do begin Result := 0; MessageDlg (E.Message, mtError, [mbOK], 0); end; end; // end except end;
In the Exception1 code, there are two different exception handlers after the same try block. You can have any number of these handlers, which are evaluated in sequence.
Using a hierarchy of exceptions, a handler is also called for the inherited classes of the type it refers to, as any procedure will do. For this reason, you need to place the broader handlers (the handlers of the ancestor Exception classes) at the end. But keep in mind that using a handler for every exception, such as the previous one, is not usually a good choice. It is better to leave unknown exceptions to Delphi. The default exception handler in the VCL displays the error message of the exception class in a message box, and then resumes normal program operation. You can modify the normal exception handler with the Application.OnException event or the OnException event of the ApplicationEvents component, as demonstrated in the ErrorLog example in the next section.
Another important element of the previous code is the use of the exception object in the handler (see on E: Exception do). The reference E of class Exception refers to the exception object passed by the raise statement. When you work with exceptions, remember this rule: You raise an exception by creating an object and handle it by indicating its type. This has an important benefit, because as you have seen, when you handle a type of exception, you are really handling exceptions of the type you specify as well as any descendant type.