I need to capture all non-unit initialization exceptions in a Delphi 7 program so I can write the exception to a file, and perhaps show the user a message.
Reading up on this, I thought that a global exception handler would be cumbersome and that all I would need is to capture all exceptions at the DPR level. However, I can't get the code below to ever get to the ShowMessage in the dpr.
Why does the Raise Exception below actually result in an exception displayed on the screen rather than bouncing out to the .dpr's except clause? Maybe a global exception handler would be a better approach?
Shouldn't the code immediately below in the dpr capture all exceptions in a form?
In DPR:
begin
Application.Initialize;
try
Application.CreateForm(TForm1, Form1);
Application.Run;
except
On E: Exception do
ShowMessage('In dpr except. Exception is: ' + E.Message);
end;
end.
In Form:
Function TForm1.DoSomething( out aErrm: String):boolean; // force a failure for testing
begin
Result := FALSE;
aErrm := 'Failed in DoSomething';
end;
procedure TForm1.FormShow(Sender: TObject);
begin
try
fOk := DoSomething(fErrm);
except
fOk := FALSE;
Errm := 'Unexpected exception'
end;
if (NOT fOk) then
Raise Exception.Create(Errm) // why does this pop-up an exception when the DPR has an except around this code?
else
PostMessage(Handle, WM_CLOSE, 0, 0); // self-closing form
end; { FormActivate }
The main message loop in TApplication.Run wraps and handles all Messages in an exception block, which as a result catches all exceptions thus rendering your primary exception block in the DPR completely useless.
If you want to capture & handle Application exceptions then use TApplication.OnException.
Related
Who is responsible for error checking and handling?
I don't have any of the expensive component libraries such as DevExpress or TMS Components etc so I cannot look at source to get an idea of how most components manage error handling.
Specifically what I am wanting to know is should there be a limit to how many errors and warnings component developers should try to capture? Is there a balance between having meaningful error checking and just making it too easy for developers using your component?
Here is an example using a few scenarios:
Note these are directly from the components source (made up for example purposes)
procedure TMyComponent.AddFromFile(FileName: string);
begin
FBitmap.LoadFromFile(FileName);
end;
or
procedure TMyComponent.AddFromFile(FileName: string);
begin
if FileExists(FileName) then
begin
FBitmap.LoadFromFile(FileName);
end
else
raise Exception.Create(FileName + ' does not exist.');
end;
And these last two are using an instance of the component at runtime:
procedure TForm1.FormCreate(Sender: TObject);
begin
MyComponent1.AddFromFile('D:\Test.bmp');
end;
or
procedure TForm1.FormCreate(Sender: TObject);
begin
if FileExists('D:\Test.bmp') then
begin
MyComponent1.AddFromFile('D:\Test.bmp');
end
else
raise Exception.Create('D:\Test.bmp does not exist.');
end;
I guess it comes down to who should error check and handle what? Is the component developer responsible for handling these types of checking or the user of the component?
As I am writing this I believe both component developer and user should handle such checking but I am unsure, so I am looking for what the general consensus amongst developers is.
Thanks.
To answer your specific queestion:
Specifically what I am wanting to know is should there be a limit to how many errors and warnings component developers should try to capture? Is there a balance between having meaningful error checking and just making it too easy for developers using your component?
The general rule about exception handling is that you should only catch exceptions you know how to handle, and let others propagate to higher code that may know how to handle it. If an exception is raised inside of your component, the component needs to decide whether to:
handle that particular exception internally and gracefully move on to other things without notifying the caller at all.
re-throw the exception (maybe with tweaks made to it), or re-throw a whole new exception, to allow the caller to identify and handle that specific failure, if desired.
ignore the exception (don't catch it at all) and just let it propagate as-is.
If an API used by your component returns an error code instead of raising an exception, the component needs to decide how to handle that as well. Whether to ignore the error and move on, or raise an exception to make it more apparent.
In your particular example, I prefer the following approach:
type
EMyComponentAddError = class(Exception)
private
FFileName: String;
begin
constructor CreateWithFileName(const AFileName: string);
property FileName: string read FFileName;
end;
constructor EMyComponentAddError.CreateWithFileName(const AFileName: string);
begin
inherited CreateFmt('Unable to add file: %s', [AFileName]);
FFileName := AFileName;
end;
procedure TMyComponent.AddFromFile(FileName: string);
begin
try
FBitmap.LoadFromFile(FileName);
except
Exception.RaiseOuterException(EMyComponentAddError.CreateWithFileName(FileName));
end;
end;
This allows your component to recognize that an error occurred, act on it as needed, and still report component-specific information to the caller without losing the original error that caused the actual failure. If the caller is interested in the details, it can catch the exception, look at its InnerException property, access custom properties if present, etc.
For example:
procedure TForm1.FormCreate(Sender: TObject);
begin
MyComponent1.AddFromFile('D:\Test.bmp');
end;
Let's assume MyComponent1.AddFromFile('D:\Test.bmp'); fails. The default exception handler will catch it and display a popup message that says:
Unable to add file: D:\Test.bmp
Useful, but little details, as it could have failed for any number of reasons. Maybe the file could not be opened, but why? Non-existant vs no permission? Maybe the file was opened but corrupted? Maybe memory could not be allocated? And so on.
The caller could catch it and display more useful info, if so desired (it is not required - the component provides the info, the caller decides whether to use it or not):
procedure TForm1.FormCreate(Sender: TObject);
begin
try
MyComponent1.AddFromFile('D:\Test.bmp');
except
on E: EMyComponentAddError do
begin
ShowMessage('There was a problem adding a file:'+sLineBreak+E.FileName+sLineBreak+sLineBreak+E.InnerException.Message);
Sysutils.Abort;
end;
end;
end;
Or:
procedure TForm1.FormCreate(Sender: TObject);
begin
try
MyComponent1.AddFromFile('D:\Test.bmp');
except
on E: EMyComponentAddError do
begin
raise Exception.CreateFmt('There was a problem adding a file:'#10'%s'#10#10'%s', [E.FileName, E.InnerException.Message]);
end;
end;
end;
Either of which would display:
There was a problem adding a file:
D:\Test.bmp
The file was not found
As David said we only need this
procedure TMyComponent.AddFromFile(FileName: string);
begin
FBitmap.LoadFromFile(FileName);
end;
This will check that
there is an existing file
in this file is a valid bitmap
Now it depends on the application, how important is this for the application. If this TForm1 is the Application.MainForm, every exception you did not catch inside the creation process will terminate the application. This is sometimes a valid behavior.
Very important, the application cannot run without
procedure TForm1.Form1Create(Sender:TObject);
begin
MyComponent.AddFromFile( 'D:\Test.bmp' );
end;
or wrap the exception for a user-friendly message
procedure TForm1.Form1Create(Sender:TObject);
begin
try
MyComponent.AddFromFile( 'D:\Test.bmp' );
except
on E: Exception do
raise Exception.Create( 'Sorry, I cannot run, because of: ' + E.Message );
end;
end;
Very important, but we have a fallback to handle this, maybe
procedure TForm1.Form1Create(Sender:TObject);
var
LBitmapFiles : TStringList;
LBitmapIdx : Integer;
LBitmapLoaded : Boolean;
LErrorStore : TStringList;
begin
LBitmapFiles := nil;
LErrorStore := nil;
try
LBitmapFiles := TStringList.Create;
LErrorStore := TStringList.Create;
LBitmapFiles.Add( 'D:\Test.bmp' );
LBitmapFiles.Add( 'D:\Fallback.bmp' );
LBitmapLoaded := False;
while not LBitmapLoaded and ( LBitmapIdx < LBitmapFiles.Count ) do
try
MyComponent.AddFromFile( LBitmapFiles[LBitmapIdx] );
LBitmapLoaded := True;
except
on E: Exception do
begin
LErrorStore.Add( LBitmapFiles[LBitmapIdx] + ': ' + E.Message );
Inc( LBitmapIdx );
end;
end;
if not LBitmapLoaded then
raise Exception.Create( 'Sorry, I cannot run, because of: ' + LErrorStore.Text );
finally
LErrorStore.Free;
LBitmapFiles.Free;
end;
end;
There are other fallbacks possible and this also depends on the application (f.i. set a dummy bitmap to the component) to get the application to work properly.
Not important, if we have no image ... we have no image, who cares
procedure TForm1.Form1Create(Sender:TObject);
const
CBitmapFile = 'D:\Test.bmp';
begin
// check, if there is a file
if FileExists( CBitmapFile ) then
try
MyComponent.AddFromFile( CBitmapFile );
except
on E: Exception do
begin
// Maybe log the exception
SomeLogger.Log( E );
// Maybe set some extra parameters for the application to know, this has failed
RunningWithoutBitmap();
end;
end
else
// Maybe set some extra parameters for the application to know, this has failed
RunningWithoutBitmap();
end;
Component
procedure TMyComponent.AddFromFile(FileName: string);
begin
FBitmap.LoadFromFile(FileName);
end;
This is all you need. If the bitmap object cannot load the file, for whatever reason, it will raise an exception. Let that exception propagate to the consumer of the code.
There's really no point trying to test whether or not the file exists. What if the file exists and it is not a bitmap file? What if the file exists, is a bitmap file, but the disk has a duff sector and the file read fails? If you attempt to check for all error conditions, you will just be repeating the checks that the LoadFromFile method already does.
Some error conditions cannot possibly be checked from the outside. An error that only becomes apparent part way through reading the file cannot reasonably be checked from the outside.
One very common consequence of over-zealous, duplicate error checking is that you end up with code that produces errors in scenarios where there should be none. If you get your error checking wrong you could end up reporting an error that would not have occurred had you let the underlying code run.
Consumer
procedure TForm1.FormCreate(Sender: TObject);
begin
MyComponent1.AddFromFile('D:\Test.bmp');
end;
At this point the decision is more difficult. I would typically expect the following question to be the driver of the decision:
Is it an expected, and reasonable event, for the file not to be present?
If the answer to that question is yes, then you should consider handling the exception in the FormCreate method. Again, testing FileExists() catches just one failure mode, albeit a common one. Perhaps you should use a try/except block to catch the error.
If the answer to the question is no, let the error propagate.
That said, you should also consider whether or not you want an exception to be thrown from your form's OnCreate event handler. That may be perfectly reasonable, but it is certainly conceivable that you will not wish to do this.
I'm having this funny issue with Delphi XE where I create a try/except/finally statement and when the application generate an exception the except block is never called it jump straight to the finally block, I tried few things like invert the try/except/finally to try/finally/except, try to change the try blocks to different places, clean the code and recompile in case was a Delphi issue but noting seems to work.
What I'm trying to accomplish here is to show a dialog message to the user and after clean up the code in case of a crash.
procedure CallbackExport(Sender: TObject);
var
SaveDlg: TSaveDialog;
FileName: string;
begin
SaveDlg := TSaveDialog.Create (nil);
try
try
SaveDlg.Title := 'Export';
SaveDlg.InitialDir := GetSystemPath(CSIDL_DESKTOP);
SaveDlg.Options := [ofOverwritePrompt, ofEnableSizing];
case (Sender as TMenuItem).Tag of
cnExcel: begin
SaveDlg.Filter := 'Excel File (*.xls)|*.xls';
end;
cnHtml: begin
SaveDlg.Filter := 'HTML File (*.html)|*.html';
end;
cnTxt: begin
SaveDlg.Filter := 'Text File (*.txt)|*.txt';
end;
cnCsv: begin
SaveDlg.Filter := 'Comma Seperated File (*.csv)';
end;
cnXml: begin
SaveDlg.Filter := 'XML file (*.xml)|*.xml';
end;
end;
if not SaveDlg.Execute(self.Handle) then
Exit;
FileName := SaveDlg.FileName;
case (Sender as TMenuItem).Tag of
cnExcel: begin
ExportGridToExcel(FileName, tvdGrid);
end;
cnHtml: begin
ExportGridToHTML(FileName, tvdGrid);
end;
cnTxt: begin
ExportGridToText(FileName, tvdGrid);
end;
cnCsv: begin
ExportGridToText(FileName, tvdGrid, true, true, ',', '', '', 'CSV');
end;
cnXml: begin
ExportGridToXML(FileName, tvdGrid);
end;
end;
except
on e: exception do
begin
ShowMessage('An error occurred while saving the file ' + FileName + #13#10 + 'With a message: ' + E.Message);
StvdAudit.tvdAudit('Error saving file, reason: ' + E.Message);
end;
end;
finally
SaveDlg.Free;
end;
end
If an exception is raised inside the try/except, and not handled by code further down the call stack, it will be caught by your exception handler.
You are claiming the ExportGridToXXX is raising an exception that is not caught by the exception handler in your code. But that claim cannot be true. Either no exception is raised, or ExportGridToXXX already handles the exception.
On the more general subject of exception handling, the general policy should be not to handle them if at all possible. You should only handle them in case you need to stop the exception propagating, and need to deal with the exception at this point in the code. Normally, particularly in a UI program, you would simply let the exception be handled by the top-level exception handler.
As well as that point, you code swallows all exceptions, irrespective of their type. That's bad practice. Supposing that you do want to handle exceptions raised by ExportGridToXXX, you should only handle the exception classes that are expected. For instance, you might encounter EAccessViolation for which your app's policy is to terminate. But since you swallowed it, treating it in the same handler used to trap sharing violations, you cannot apply that policy. Always be discerning in your handling of exceptions.
Do the Export(smth) functions reside in a separate DLL? Then your application's Exception class is not the same as external DLL's Exception class.
Your exception handler is swallowing the exception, try re-raising instead:
on e: exception do
begin
StvdAudit.tvdAudit('Error saving file, reason: ' + E.Message);
raise exception.create('An error occurred while saving the file ' + FileName + #13#10 + 'With a message: ' + E.Message);
end;
I wrote a TThread descendant class that, if an exception is raised, saves exception's Class and Message in two private fields
private
//...
FExceptionClass: ExceptClass; // --> Class of Exception
FExceptionMessage: String;
//...
I thought I could raise a similar exception in the OnTerminate event, so that the main thread could handle it (here is a simplified version):
procedure TMyThread.Execute;
begin
try
DoSomething;
raise Exception.Create('Thread Exception!!');
except
on E:Exception do
begin
FExceptionClass := ExceptClass(E.ClassType);
FExceptionMessage := E.Message;
end;
end;
end;
procedure TMyThread.DoOnTerminate(Sender: TObject);
begin
if Assigned(FExceptionClass) then
raise FExceptionClass.Create(FExceptionMessage);
end;
I expect that the standard exception handling mechanism occurs (an error dialog box),
but I get mixed results: The dialog appears but is followed by a system error, or
or (more funny) the dialog appears but the function that called the thread goes on as if the exception were never raised.
I guess that the problem is about the call stack.
Is it a bad idea?
Is there another way to decouple thread exceptions from the main thread but reproducing them the standard way?
Thank you
The fundamental issue in this question, to my mind is:
What happens when you raise an exception in a thread's OnTerminate event handler.
A thread's OnTerminate event handler is invoked on the main thread, by a call to Synchronize. Now, your OnTerminate event handler is raising an exception. So we need to work out how that exception propagates.
If you examine the call stack in your OnTerminate event handler you will see that it is called on the main thread from CheckSynchronize. The code that is relevant is this:
try
SyncProc.SyncRec.FMethod; // this ultimately leads to your OnTerminate
except
SyncProc.SyncRec.FSynchronizeException := AcquireExceptionObject;
end;
So, CheckSynchronize catches your exception and stashes it away in FSynchronizeException. Excecution then continues, and FSynchronizeException is later raised. And it turns out, that the stashed away exception is raised in TThread.Synchronize. The last dying act of TThread.Synchronize is:
if Assigned(ASyncRec.FSynchronizeException) then
raise ASyncRec.FSynchronizeException;
What this means is that your attempts to get the exception to be raised in the main thread have been thwarted by the framework which moved it back onto your thread. Now, this is something of a disaster because at the point at which raise ASyncRec.FSynchronizeException is executed, in this scenario, there is no exception handler active. That means that the thread procedure will throw an SEH exception. And that will bring the house down.
So, my conclusion from all this is the following rule:
Never raise an exception in a thread's OnTerminate event handler.
You will have to find a different way to surface this event in your main thread. For example, queueing a message to the main thread, for example by a call to PostMessage.
As an aside, you don't need to implement an exception handler in your Execute method since TThread already does so.
The implementation of TThread wraps the call to Execute in an try/except block. This is in the ThreadProc function in Classes. The pertinent code is:
try
Thread.Execute;
except
Thread.FFatalException := AcquireExceptionObject;
end;
The OnTerminate event handler is called after the exception has been caught and so you could perfectly well elect to re-surface it from there, although not by naively raising it as we discovered above.
Your code would then look like this:
procedure TMyThread.Execute;
begin
raise Exception.Create('Thread Exception!!');
end;
procedure TMyThread.DoOnTerminate(Sender: TObject);
begin
if Assigned(FatalException) and (FatalException is Exception) then
QueueExceptionToMainThread(Exception(FatalException).Message);
end;
And just to be clear, QueueExceptionToMainThread is some functionality that you have to write!
AFAIK the OnTerminate event is called with the main thread (Delphi 7 source code):
procedure TThread.DoTerminate;
begin
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
end;
The Synchronize() method is in fact executed in the CheckSynchronize() context, and in Delphi 7, it will re-raise the exception in the remote thread.
Therefore, raising an exception in OnTerminate is unsafe or at least without any usefulness, since at this time TMyThread.Execute is already out of scope.
In short, the exception will never be triggered in your Execute method.
For your case, I suspect you should not raise any exception in OnTerminate, but rather set a global variable (not very beautiful), add an item in a thread-safe global list (better), and/or raise a TEvent or post a GDI message.
A synchonized call of an exception will not prevent the thread from being interrupted.
Anything in function ThreadProc after Thread.DoTerminate; will be omitted.
The code above has two test cases
One with commented / uncommented synchronized exception //**
The second with (un)encapsulated exception in the OnTerminate event, which will lead even to an omitted destruction if used unencapsulated.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMyThread=Class(TThread)
private
FExceptionClass: ExceptClass;
FExceptionMessage: String;
procedure DoOnTerminate(Sender: TObject);
procedure SynChronizedException;
procedure SynChronizedMessage;
public
procedure Execute;override;
Destructor Destroy;override;
End;
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TMyThread.SynChronizedException;
begin
Showmessage('> SynChronizedException');
raise Exception.Create('Called Synchronized');
Showmessage('< SynChronizedException'); // will never be seen
end;
procedure TMyThread.SynChronizedMessage;
begin
Showmessage('After SynChronizedException');
end;
procedure TMyThread.Execute;
begin
try
OnTerminate := DoOnTerminate; // first test
Synchronize(SynChronizedException); //** comment this part for second test
Synchronize(SynChronizedMessage); // will not be seen
raise Exception.Create('Thread Exception!!');
except
on E:Exception do
begin
FExceptionClass := ExceptClass(E.ClassType);
FExceptionMessage := E.Message;
end;
end;
end;
destructor TMyThread.Destroy;
begin
Showmessage('Destroy ' + BoolToStr(Finished)) ;
inherited;
end;
procedure TMyThread.DoOnTerminate(Sender: TObject);
begin
{ with commented part above this will lead to a not called destructor
if Assigned(FExceptionClass) then
raise FExceptionClass.Create(FExceptionMessage);
}
if Assigned(FExceptionClass) then
try // just silent for testing
raise FExceptionClass.Create(FExceptionMessage);
except
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
With TMyThread.Create(false) do FreeOnTerminate := true;
ShowMessage('Hallo');
end;
end.
I don't know why you want to raise the exception in the main thread, but I will assume it is to do minimal exception handling - which I would consider to be something like displaying the ClassName and Message of the Exception object in a nice way on the UI. If this is all you want to do then how about if you catch the exception in your thread, then save the Exception.ClassName and Exception.Message strings to private variables on the main thread. I know it's not the most advanced method, but I've done this and I know it works. Once the thread terminates because of the exception you can display those 2 strings on the UI. All you need now is a mechanism for notifying the main thread that the worker thread has terminated. I've achieved this in the past using Messages but I can't remember the specifics.
Rather than try to solve "How do I solve problem A by doing B?" you could reframe your situation as "How do I solve problem A whichever way possible?".
Just a suggestion. Hope it helps your situation.
I have a threaded application and for some purpose I want to pass call stack information of a catched exception to a new custom exception:
try
//here an unknown exception is rissen
except
on E: Exception do
begin
if ... then
raise EMyException.Create(E, CallStackOfExceptionEAsString);
end;
end;
What is the best way to do this, preferably using EurekaLog? I am using Delphi 2006 btw.
EurekaLog exposes several event handlers like OnExceptionNotify.
You can implement these in your code. For example: procedure EurekaLogExceptionNotify(
EurekaExceptionRecord: TEurekaExceptionRecord; var Handled: Boolean);
Here you can see a TEurekaExceptionRecord which is defined in ExceptionLog.pas. But you maybe just own the non-source version which works just fine.
The record has a EurekaExceptionRecord.CallStack list. This proprietary list can be converted to TStringsusing the CallStackToStrings method which is also defined in the ExceptionLog unit.
Here is an example where I write the CallStack into a StringList.
CallStackList := TStringList.Create;
try
CallStackToStrings(EurekaExceptionRecord.CallStack, CallStackList);
LogMessage := 'An unhandled exception occured. Here is the CallStack.' + #13#10
+ CallStackList.Text;
finally
CallStackList.Free;
end;
At least from this starting point you should be able to investigate the exposed functions, records etc.. All information is accessible.
EurekaLog provides a function GetLastExceptionCallStack() (defined in unit ExceptionLog.pas).
Using this I have written the following function (based on example code here):
function GetLastEurekalogCallStackAsString(): string;
{$IFDEF EUREKALOG}
var
Stack: TEurekaStackList;
Str: TStringList;
{$ENDIF}
begin
{$IFDEF EUREKALOG}
Stack := GetLastExceptionCallStack();
try
Str := TStringList.Create;
try
CallStackToStrings(Stack, Str);
Result := Str.Text;
finally
FreeAndNil(Str);
end;
finally
FreeAndNil(Stack);
end;
{$ELSE}
Result := '';
{$ENDIF}
end;
So you can write:
try
//here an unknown exception is rissen
except
on E: Exception do
begin
if ... then
raise EMyException.Create(E, GetLastEurekalogCallStackAsString());
end;
end;
EurekaLog 7 has Chained Exception support, which is specifically designed for this task. Just enable it in options (it is enabled by default) and use:
try
// here an unknown exception is rissen
except
on E: Exception do
begin
if ... then
Exception.RaiseOuterException(EMyException.Create(E.Message));
// for old IDEs:
// raise EMyException.Create(E.Message);
end;
end;
Update: It seems to be specific to D2007. It works in D2010 like it worked in older version.
I would like to return an exit code depending on the type of Exception caught in the Eception Handler block like:
program test;
{$APPTYPE CONSOLE}
uses
SysUtils;
var
Exitcode: Integer;
begin
Writeln('Enter error code:');
Readln(Exitcode);
try
raise EExternal.Create('sdsdkfjh');
except
on E:EExternal do
begin
Writeln(E.Classname, ': ', E.Message);
Halt(Exitcode);
end;
end;
end.
Unfortunately in D2007, calling Halt(n) from an Exception block always returns an Exit code 1, no matter what you pass to Halt().
Apparently because exiting from an Exception handler calls Finalize, which clears the pending (non Abort) Exceptions, calling SysUtils.ExceptHandler:
procedure ExceptHandler(ExceptObject: TObject; ExceptAddr: Pointer); far;
begin
ShowException(ExceptObject, ExceptAddr);
Halt(1); // <= ##$####$!
end;
And no matter what exit code I wanted I get that Halt(1)!
So the question is:
How can I simply return the desired Exit code depending on which Exception was raised?
Will this work?
NeedHalt := False;
try
raise EExternal.Create('sdsdkfjh');
except
on E:EExternal do
begin
Writeln(E.Classname, ': ', E.Message);
NeedHalt := True;
end;
end;
if NeedHalt then
Halt(Exitcode);
Or this?
try
raise EExternal.Create('sdsdkfjh');
except
on E:EExternal do
begin
Writeln(E.Classname, ': ', E.Message);
AcquireExceptionObject;
Halt(Exitcode);
end;
end;
Anyway: it's a bug in D2007, which was fixed in D2010.
Actually... it seems to work as intended....
I used your code...
program test1;
{$APPTYPE CONSOLE}
uses
SysUtils;
var
Exitcode: Integer;
begin
Writeln('Enter error code:');
Readln(Exitcode);
try
raise EExternal.Create('sdsdkfjh');
except
on E:EExternal do
begin
Writeln(E.Classname, ': ', E.Message);
Halt(Exitcode);
end;
end;
end.
Compiled in in Delphi 5, then ran it in a DOS box under XP...
C:\>test1
Enter error code:
111
EExternal: sdsdkfjh
C:\>echo %errorlevel%
111
C:\>
Note that DOS Error Levels are restricted to the range of 0 to 65535. Echoing %errorlevel% is the quickest way to see the error level.
Don't forget that reading the errorlevel clears it.
If you want to immediately abort the program without any cleanup, and return an exit code, try ExitProcess. See the article for a few caveats on using ExitProcess, though.
Using halt(I) generates memory leaks (you can see that if you enabled the FastMM MemoryLeaks with ReportMemoryLeaksOnShutdown:=true;)
It's much better to use a "Clean" Exit and set ExitCode prior to exiting.
In a main section of a console app for instance:
ExitCode:=I;
exit;
If the built-in exception-handling function doesn't do what you like, then replace it with your own:
function ExitCodeExceptHandler(ExceptObject: TObject; ExceptAddr: Pointer);
begin
ShowException(ExceptObject, ExceptAddr);
if ExitCode = 0 then
ExitCode := 1;
Halt(ExitCode);
end;
Assign that to the global System.ExceptProc variable when your program starts:
ExceptProc := #ExitCodeExceptHandler;
I've implemented it to use the global ExitCode variable. If it's still at its default value of 0, then the function reverts to the original Delphi behavior of exiting with 1, but if the exit code has already been set to something else, then this will halt with that value instead. The first thing Halt does is set the global ExitCode variable, so your code should need no further changes (although I'd choose a different name for the Exitcode variable). Your call to Halt will set the global ExitCode variable and then proceed to shut down the program. The exception handler will notice that ExitCode is already set and re-call Halt with that value instead of 1.