Title: Exception Handling in Delphi - Framework to log the complete traverse information
Question: One of the hiccups in huge applications developed in Delphi is tracing exceptions till the root. Delphi as such provides a lovely mechanism to trap exceptions by way of try..except blocks. But then, in order to utilize this mechanism we need to have a proper framework. In this write up I am explaining the framework designed by me for handling exceptions. In this mechanism we get the complete information like the method names, unit names and the traverse information of a particular exception. The only limitation being the line number for which we might need to provide the Debug information or Map files. This frame work is designed to work under all the compiling/linking conditions and so does not include the mechanism to provide the line numbers.
Also, expecting opinions and responses to make it much better.
Answer:
To have a proper control of the exceptions in an Application, a new class is created - ECustomException class with Exception as its parent class. An array of Pointer is introduced in this class basically to store the error details that are thrown within the application. This class has two constructors, the need for both will be known when we implementation part. A record has been introduced which can be changed by the implementor based on his requirements. Currently the record stores the Unit Name, Method and Module name.
Before we get down to the actual code the let me explain the guidelines kept in mind when designing the framework:
a) A complete traverse information of the exception.
b) A mechanism to display a meaningul information to the end user, but a detailed technical information to the developer.
c) Centralized Exception displaying and logging mechanism (Application.OnException method).
{******************************************************************************}
{ }
{ Custom Exception class }
{ Author: Sri Ram Ambuga Nandakumar }
{ }
{ Disclaimer }
{ ---------- }
{ }
{ THE FILES ARE PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND WHETHER }
{ EXPRESSED OR IMPLIED. }
{ }
{ In no event shall the author be held liable for any damages whatsoever, }
{ including without limitation, damages for loss of business profits, }
{ business interruption, loss of business information, or any other loss }
{ arising from the use or inability to use the unit. }
{******************************************************************************}
unit UExceptionObj;
interface
uses
SysUtils;
type
PErrorDetails = ^TErrorDetails; //----------------------------------
TErrorDetails = record // | Error Detail record.
sUnit: string; // |- Can be changed by the implementor
sModule: string; // | To suit his requirement
sMethod: string; // |
end; //----------------------------------
TCallStack = array of PErrorDetails; //
type
ECustomException = class(Exception)
private
FCallStack: TCallStack;
function GetStackLength: integer;
function GetErrorDetails(Index: integer): TErrorDetails;
protected
public
constructor Create(AMessage: string; AErrorDetails: TErrorDetails); overload;
constructor Create(AMessage: string; AException: ECustomException); overload;
destructor Destroy; override;
procedure AddToCallStack(AErrorDetails: TErrorDetails);
property StackLength: integer read GetStackLength;
property ErrorDetails[Index: integer]: TErrorDetails read GetErrorDetails; default;
end;
implementation
{ ECustomException }
constructor ECustomException.Create(AMessage: string;
AErrorDetails: TErrorDetails);
begin
inherited Create(AMessage);
AddToCallStack(AErrorDetails);
end;
procedure ECustomException.AddToCallStack(AErrorDetails: TErrorDetails);
var
ptrErrDet: PErrorDetails;
begin
New(ptrErrDet);
ptrErrDet^ := AErrorDetails;
SetLength(FCallStack, High(FCallStack) + 2);
FCallStack[High(FCallStack)] := ptrErrDet;
end;
constructor ECustomException.Create(AMessage: string;
AException: ECustomException);
var
iCount: integer;
begin
inherited Create(AMessage);
for iCount := Low(AException.FCallStack) to High(AException.FCallStack) do
AddToCallStack(AException.FCallStack[iCount]^);
end;
destructor ECustomException.Destroy;
var
iCount: integer;
begin
inherited;
for iCount := Low(FCallStack) to High(FCallStack) do
Dispose(FCallStack[iCount]);
if Assigned(FCallStack) then
Finalize(FCallStack);
end;
function ECustomException.GetStackLength: integer;
begin
Result := High(FCallStack);
end;
function ECustomException.GetErrorDetails(Index: integer): TErrorDetails;
begin
if (Index -1) and (Index Result := FCallStack[Index]^;
end;
end.
//****************************************************************************************************************
As we can see the Exception class takes care of storing the error information that are given to it. Now coming to the interface function which should be called from all the procedures in the Application.
//****************************************************************************************************************
procedure GlobalExceptionHandler(AException: Exception; sUnit, sModule,
sProcedure: string);
var
recErrDet: TErrorDetails;
begin
recErrDet.sModule := sModule;
recErrDet.sProcedure := sProcedure;
recErrDet.sUnit := sUnit;
if AException is ECustomException then
begin
{ Here we can see if it is ECustomException then it adds the entry in the Exception object and then raises the same error.
Note: since raise creates a new object the earlier exception's stack info has to be passed on to the newly created one.
This is the purpose of one of the Constructor.
}
(AException as ECustomException).AddToCallStack(recErrDet);
raise ECustomException.Create(AException.Message, (AException as ECustomException));
end
else
{ If it is any other Error then create our exception class with the earlier exception's message. This happens only at the source of the
error only that is for the first time. Additional code can be written here to change the message of certain Exceptions like
if EAccessViolation then raise ECustomException.Create('An error occurred while accessing a resource', recErrDet); ;) }
raise ECustomException.Create(AException.Message, recErrDet);
end;
//****************************************************************************************************************
Additionally, this global exception handler can be fine tuned as per the requirement of the application.
In the Application.OnException method just a loop will give all the information about the exception.
Now all the methods in my application would look like below:
procedure TForm1.Button1Click(Sender: TObject);
begin
try
CallAProcedure
except
on E: Exception do
GlobalExceptionHandler(E, 'UnitName', Application.ExeName, 'Button1Click');
end;
end;
procedure CallAProcedure;
begin
try
CallRaiseAnException;
except
on E: Exception do
GlobalExceptionHandler(E, 'UnitName', Application.ExeName, 'CallAProcedure');
end;
end;
procedure CallRaiseAnException;
begin
try
RaiseAnException;
except
on E: Exception do
GlobalExceptionHandler(E, 'UnitName', Application.ExeName,
'CallRaiseAnException');
end;
end;
procedure RaiseAnException;
begin
try
raise Exception.Create('This is the original error message');
except
on E: Exception do
GlobalExceptionHandler(E, 'UnitName', Application.ExeName,
'CallRaiseAnException');
end;
end;
Now in the above case when the exception object reaches the Application's
OnException method, the information of procedures that it will contain will be:
Button1Click
CallAProcedure
CallRaiseAnException
RaiseAnException.
Please let me know your opinions and views regarding the same
Sri Ram Ambuga Nandakumar