Exceptions
An exception is generally an error condition or other event that interrupts normal flow of execution in an application. When an exception is raised, it causes control to be transferred from the current point of execution to an exception handler. Object Pascal's exception handling support provides a structured means of separating normal program logic from error handling logic, greatly increasing the maintainability and robustness of applications.
Object Pascal uses objects to represent exceptions. This has several advantages, the key ones of which are
-
Exceptions can be grouped into hierarchies using inheritance
-
New exceptions can be introduced without affecting existing code
-
An exception object can carry information (such as an error message
or an error code) from the point where it was raised to the point where it is handled
Using exception handling
The SysUtils unit implements exception generation and handling for the Object Pascal run-time library. When an application uses the SysUtils unit, all run-time errors are automatically converted into exceptions. This means that error conditions such as out of memory, division by zero, and general protection fault, which would otherwise terminate an application, can be caught and handled in a structured fashion.
Note Delphi's Visual Class Library fully supports exception handling. An application that uses VCL automatically also uses the SysUtils unit, thus enabling exception handling.
Exception declarations
An exception in Object Pascal is simply a class, and the declaration of an exception is no different than the declaration of an ordinary class. Although it is possible to use an instance of any class as an exception object, it is recommended that all exceptions be derived from the Exception class defined in the SysUtils unit. For example
type
EMathError = class(Exception); EInvalidOp = class(EMathError); EZeroDivide = class(EMathError); EOverflow = class(EMathError); EUnderflow = class(EMathError);
Exceptions are often grouped into families of related exceptions using inheritance. The above declarations (which were extracted from the SysUtils unit) define a family of related math error exceptions. Using families, it is possible to handle an entire group of exceptions under one name. For example, an exception handler for EMathError will also handle EInvalidOp, EZeroDivide, EOverflow, and EUnderflow exceptions, and any user-defined exceptions that directly or indirectly derive from EMathError.
Exception classes sometimes define additional fields, methods, and properties used to convey additional information about the exception. For example, the EInOutError class defined in the SysUtils unit introduces an ErrorCode field which contains the file I/O error code that caused the exception.
type
EInOutError = class(Exception) ErrorCode: Integer;
end;
The raise statement
An exception is raised using a raise statement.
raise statement
The argument to a raise statement must be an object. In other words, the raise keyword must be followed by an expression of a class type. When an exception is raised, the exception handling logic takes ownership of the exception object. Once the exception is handled, the exception object is automatically destroyed through a call to the object's Destroy destructor. An application should never attempt to manually destroy a raised exception object.
Note A raise statement raises an object, not a class. The argument to a raise statement is typically constructed "on the fly" through a call to the Create constructor of the appropriate exception class.
A raise statement that omits the exception object argument will re-raise the current exception. This form of a raise statement is allowed only in an exception block, and is described further in the section entitled "Re-raising exceptions".
Control never returns from a raise statement. Instead, raise transfers control to the innermost exception handler that can handle exceptions of the given class.
Innermost in this case means the handler whose try...except block was most recently entered and not yet exited.
The StrToIntRange function below converts a string to an integer, and raises an
ERangeError exception if the resulting value is not within a specified range.
function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
Result := StrToInt(S);
if (Result < Min) or (Result > Max) then raise ERangeError.CreateFmt(
'%d is not within the valid range of %d..%d', [Result, Min, Max]);
end;
The try...except statement
Exceptions are handled using try...except statements.
try statement
statement list
exception block
exception handler
A try...except statement executes the statements in the try statement list in sequential order. If the statements execute without any exceptions being raised, the exception
block is ignored, and control is passed to the statement following the end keyword that ends the try...except statement.
The exception block in the except...end section defines exception handlers for the try statement list. An exception handler can be invoked only by a raise statement executed in the try statement list or by a procedure or function called from the try statement list.
When an exception is raised, control is transferred to the innermost exception handler that can handle exceptions of the given class. The search for an exception handler starts with the most recently entered and not yet exited try...except statement. If that try...except statement cannot handle exceptions of the given class, the next most recently entered try...except statement is examined. This propagation of the exception continues until an appropriate handler is found, or there are no more active try...except statements. In the latter case a run-time error occurs and the application is terminated.
To determine whether the exception block of a try...except statement can handle a particular exception, the on...do exception handlers are examined in order of appearance. The first exception handler that lists the exception class or a base class of the exception is considered a match. If an exception block contains an else part, and if none of the on...do exception handlers match the exception, the else part is considered a match. An exception block that contains only a statement list is considered a match for any exception.
Once a matching exception handler is found, the stack is "unwound" to the procedure or function that contains the handler, and control is transferred to the handler. The unwinding process will discard all procedure and function calls that occurred since entering the try...except statement containing the exception handler.
Following execution of an exception handler, the exception object is automatically destroyed through a call to the object's Destroy destructor, and control is passed to the statement following the end keyword that ends the try...except statement.
In the example below, the first on...do handles division by zero exceptions, the second on...do handles overflow exceptions, and the final on...do handles all other math exceptions.
try
...
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
end;
As described earlier, to locate a handler for a given exception class, the on...do handlers are processed in order of appearance. This means that handlers for the most derived classes should be listed first. For example, since EZeroDivide and EOverflow are both derived from EMathError, a handler for EMathError will also handle EZeroDivide and EOverflow exceptions. If EMathError was listed before these exceptions, the more specific handlers would never be invoked.
An on...do exception handler can optionally specify an identifier and a colon before the exception class identifier. This declares the identifier to represent the exception object during execution of the statement that follows on...do. For example
try
...
except
on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;
The scope of an identifier declared in an exception handler is the statement that follows on...do. The identifier hides any similarly named identifier in an outer scope.
If a try...except statement specifies an else part, then any exceptions that aren't handled by the on...do exception handlers will be handled by the else part. For example
try
...
except
on EZeroDivide do HandleZeroDivide;
on EOverflow do HandleOverflow;
on EMathError do HandleMathError;
else
HandleAllOthers;
end;
Here, the else part will handle any exception that isn't an EMathError.
An exception block that contains no on...do handlers, but instead consists only of a list of statements, will handle all exceptions.
try
...
except
HandleException;
end;
Here, any exception that occurs as a result of executing the statements between try
and except will be handled by the HandleException procedure.
Re-raising exceptions
In some situations, a procedure or function may need to perform clean-up operations when an exception occurs, but the procedure or function may not be prepared to actually handle the exception. For example, consider the GetFileList function below, which allocates a TStringList object and fills it with the filenames matching a given search path.
function GetFileList(const Path: string): TStringList;
var
I: Integer;
SearchRec: TSearchRec;
begin
Result := TStringList.Create;
try
I := FindFirst(Path, 0, SearchRec);
while I = 0 do begin
Result.Add(SearchRec.Name); I := FindNext(SearchRec);
end; except
Result.Free;
raise; end;
end;
The function first allocates a new TStringList object, and then uses the FindFirst and FindNext functions (defined in the SysUtils unit) to initialize the string list. If, for any reason, the initialization of the string list fails, for exampe because the given search path is invalid, or because there is not enough memory to fill in the string list, GetFileList needs to dispose the newly allocated string list, since the caller does not yet know of its existence. For that reason, the initialization of the string list is performed in a try...except statement. If an exception occurs, the exception handler part of the try...except statement disposes the string list, and then re-raises the exception.
Nested exceptions
Code executed in an exception handler can itself raise and handle exceptions. As long as exceptions raised in an exception handler are also handled within the exception handler, they do not affect the original exception. However, once an exception raised in an exception handler propagates beyond that handler, the original exception is lost. This is illustrated by the Tan function shown below.
type
ETrigError = class(EMathError);
function Tan(X: Extended): Extended;
begin try
Result := Sin(X) / Cos(X);
except
on EMathError do
raise ETrigError.Create('Invalid argument to Tan');
end; end;
If an EMathError is raised in the computation of the tangent of the given angle, such as the EZeroDivide that would result if Cos(X) is zero, the exception handler raises an ETrigError exception. Since the Tan function does not provide a handler for the ETrigError exception that it raises, the ETrigError will propagate beyond the original
exception handler, causing the EMathError exception to be disposed. To the caller, the result is simply that the Tan function has raised an ETrigError exception.
The try...finally statement
When a section of code acquires a resource, it is often necessary to ensure that the resource be released again, regardless of whether the code completes normally or is interrupted by an exception. For example, a section of code that opens and processes a file will normally want to ensure that the file is closed no matter how the code terminates. The try...finally statement can be used in such situations.
try statement
statement list
A try...finally statement executes the statements in the try statement list in sequential order. If no exceptions are raised in the try statement list, the finally statement list is executed. If an exception is raised in the try statement list, control is transferred to the finally statement list, and once the finally statement list completes execution, the exception is re-raised. The resulting effect is that the finally statement list is always executed, regardless of how the try statement list terminates.
The section of code shown below illustrates how a try...finally statement can be used to ensure that a file that was opened is always closed.
Reset(F);
try
ProcessFile(F);
finally
CloseFile(F);
end;
Note In a typical application, try...finally statements such as the one above tend to be much more common than try...except statements. Applications written using Delphi's Visual Class Library, for example, normally rely on VCL's default exception handling mechanisms, thus seldom needing to use try...except statements. Resource allocations will however often have to be guarded against exceptions, and the use of try...finally statements will therefore be much more frequent.
If an exception is raised but not handled in the finally statement list, that exception is propagated out of the try...finally statement, and any original exception is lost.
Note It is strongly recommended that a finally statement list always handle all local exceptions, so as to not disturb the propagation of an external exception.
Exit, Break, and Continue procedures
If a call to one of the Exit, Break, or Continue standard procedures cause control to leave the try statement list of a try...finally statement, the finally statement list is automatically executed. Likewise, if one of these standard procedures are used to leave an exception handler, the exception object is automatically disposed.
Predefined exceptions
The SysUtils unit declares a number of exception classes, including a class named Exception which serves as the ultimate ancestor of all exception classes. The public interface of Exception is declared as follows
type
Exception = class(TObject) public
constructor Create(const Msg: string);
constructor CreateFmt(const Msg: string; const Args: array of const); constructor CreateRes(Ident: Word);
constructor CreateResFmt(Ident: Word; const Args: array of const); constructor CreateHelp(const Msg: string; HelpContext: Longint); constructor CreateFmtHelp(const Msg: string; const Args: array of const;
HelpContext: Longint);
constructor CreateResHelp(Ident: Word; HelpContext: Longint);
constructor CreateResFmtHelp(Ident: Word; const Args: array of const; HelpContext: Longint);
destructor Destroy; override; property HelpContext: Longint; property Message: string;
end;
The Exception class establishes two properties, Message and HelpContext, that every exception object inherits. This means that an arbitrary exception object can at least provide a descriptive message of the exception condition, and possibly a help context that refers to further on-line help on the topic.
The constructors defined by Exception provide various ways of initializing the
Message and HelpContext properties. In general
-
Constructors that don't include "Res" in their names require the
exception message to be specified as a string parameter. Those that do include "Res" in their names will initialize the Message property from the string resource with the given ID.
-
Constructors that include "Fmt" in their names interpret the
specified exception message as a format string, and require an extra Args parameter which supplies the format arguments. The initial value of the Message property is constructed through a call to the Format function in the SysUtils unit.
-
Constructors that include "Help" in their names require an extra
HelpContext
parameter which supplies the on-line help context for the exception.
The following table lists all exceptions defined by the SysUtils unit, and the situations in which the exceptions are raised. Unless otherwise noted, the exceptions are direct descendants of the Exception class.
Table 10-1 Predefined exception classes
EAbort The "silent exception" raised by the Abort procedure.
EOutOfMemory Raised if there is not enough memory for a particular operation.
EInOutError Raised if a file I/O operation causes an error. The EInOutError exception defines an ErrorCode field that contains the I/O error code, corresponding to the value returned by the IOResult standard function.
EIntError The ancestor class for all integer arithmetic exceptions.
EDivByZero Derived from EIntError. Raised if an integer divide operation with a zero divisor is attempted.
ERangeError Derived from EIntError. Raised if a range check operation fails in a section of code that was compiled in the {$R+} state.
EIntOverflow Derived from EIntError. Raised if an overflow check operation fails in a section of code that was compiled in the {$Q+} state.
EMathError The ancestor class for all floating-point math exceptions.
EInvalidOp Derived from EMathError. Raised if an invalid math operation is performed, such as taking the square root of a negative number.
EZeroDivide Derived from EMathError. Raised if a divide operation with a zero divisor is attempted.
EOverflow Derived from EMathError. Raised if a floating-point operation produces an overflow.
EUnderflow Derived from EMathError. By default, floating-point operations that underflow simply produce a zero result. An application must manually change the control word of the 80x87 co-processor to enable underflow exceptions.
EInvalidPointer Raised if an application attempts to free an invalid pointer.
EInvalidCast Raised if the object given on the left hand side of an as operator is not of the class given on the right hand side of the operator.
EConvertError Raised if a conversion function cannot perform the required conversion. A number of functions in the SysUtils unit, such as StrToInt, StrToFloat, and StrToDateTime, may raise this exception.
EProcessorException The ancestor class for all hardware exceptions.
EFault Derived from EProcessorException. The ancestor class for all processor fault exceptions.
EGPFault Derived from EFault. Raised if an application tries to access an invalid memory address. This exception typically indicates that the application tried to access an object through an uninitialized object reference, or tried to dereference an uninitialized pointer.
EStackFault Derived from EFault. Raised if there is not enough stack space to allocate the local variables for a procedure or function. Stack overflow checking must be enabled, using a {$S+} directive, for this exception to occur.
EPageFault Derived from EFault. Raised if the CPU reports a page fault.
EInvalidOpCode Derived from EFault. Raised if the CPU detects an invalid instruction.
EBreakpoint Derived from EProcessorException. Raised if the CPU encounters a breakpoint instruction.
ESingleStep Derived from EProcessorException. Raised after the CPU has executed an instruction in single-step mode.
Exception handling support routines
The SysUtils unit defines a number of exception handling support routines. A brief description of each is presented here. For further information, see the Visual Component Library Reference.
Table 10-2 Exception support routines
ExceptObject Returns a reference to the current exception object, that is the object associated with the currently raised exception. If there is no current exception, ExceptObject returns nil.
ExceptAddr Returns the address at which the current exception was raised. If there is no current exception, ExceptAddr returns nil.
ShowException Displays an exception dialog box for a given exception object and exception address.
Abort Raises an EAbort exception. VCL's standard exception handler treats EAbort as a "silent exception", and does not display an exception dialog box when it is handled.
OutOfMemoryError Raises an EOutOfMemory error. OutOfMemoryError uses a pre-allocated EOutOfMemory exception object, thus avoiding any dynamic memory allocations as part of raising the exception.
C h a p t e r