User friendly exception error delphi - delphi

I have this method where i execute a sql statement and catch a error in a try except statement
AdoQuery := TAdoQuery.Create(self);
AdoQuery.connection := AdoConnection;
AdoQuery.SQL.Add(sqlStr);
AdoQuery.Prepared := true;
try
begin
AdoQuery.ExecSql;
AdoQuery.Active := false;
end;
except on e:eAdoError do
ShowMessage('Error while creating the table: ' + e.Message);
end;
I can catch the error like this and show it to the user but it's showing some useless info for the user. I Would like to show only the %msg part of the error, take a look at the pic:
I tought e.MEssage allow me to get only the %msg part but it give me the whole thing hardly understoodable by a random user. How do i get only the usefull info in this case
Table reftabtest.rPCE already exists
Thank you.

You can use the Errors property of the TADOConnection object, what you want is the Description member of the Error object.
In your case:
function ParseOBDCError(ErrorDescription : String) : String;
var
Ps : Integer;
Pattern : String;
begin
Pattern := '%msg:';
Ps := Pos(Pattern, ErrorDescription);
if Ps > 0 then
begin
Result := Copy(ErrorDescription, Ps+Length(Pattern)+1);
// if you want, you can clean out other parts like < and >
Result := StringReplace(Result, '<', , '', [rfReplaceAll]);
Result := StringReplace(Result, '>', , '', [rfReplaceAll]);
Result := Trim(Result);
end
else
Result := ErrorDescription;
end;
...
AdoQuery := TAdoQuery.Create(self);
AdoQuery.connection := AdoConnection;
AdoQuery.SQL.Add(sqlStr);
AdoQuery.Prepared := true;
try
AdoQuery.ExecSql;
AdoQuery.Active := false;
except on e : Exception do
begin
if AdoConnection.Errors.Count > 0 then
ShowMessageFmt('Error while creating the table: %s',
[ParseOBDCError(AdoConnection.Errors[0].Description)])
else
ShowMessageFmt('something went wrong here: %s', [e.Message]);
end;
end;

That message dialog wasn't shown by your ShowMessage() code.
First, the icon is wrong - that's not the ShowMessage icon.
Second, the text you added ('Error while creating the table: ') to the message is missing.
This means your exception swallower is not catching the exception, because it's not of the EADOError class. So what's happening is the application's default exception handler is showing the exception.
Before I explain how to fix it, I need to point out that your exception swallower is wrong (your's should not be misnamed an exception handler).
Because you're swallowing the exception: If another method calls yours it will incorrectly think you method succeeded, and possibly do something it shouldn't. You should never write code that makes an assumption that there isn't a significant call stack leading into your method.
Swallowing to show a message to the user doesn't help, because it hides the error from the rest of the program. Especially since, as you can see: there is already code in one place that tells the user about the error without hiding it from the rest of the program. (The problem you have is that you want a friendlier message.)
To fix it:
First find out what the actual exception class is, so you're able to catch the correct error.
Now you have a number of options, but the simplest is as follows:
First log the exception, preferably with call stack. You don't want to be stuck in the situation where the user gets a friendly message but you as developer lose critical information if you need to do some debugging.
To get the call stack you can consider third-party tools like Mad Except, Exceptional Magic, JCLDebug to name a few.
Now show the message to the user.
Finally call Abort. This raises an EAbort exception which by convention is a "silent exception". It tells the rest of the program that there was an error (so it doesn't do things assuming everything is fine). But by convention, any further exception handlers should not show another message to the user. This includes the default handler's dialog in your question.
If the default handler is incorrectly showing EAbort messages, then it should be fixed.

Related

Exception on FormCreate is not thrown

By default any unhandled exception occurring in TForm.OnCreate will lead to an error message beeing shown instead of an exception beeing thrown.
The reason is this code in VCL.Forms:
function TCustomForm.HandleCreateException: Boolean;
begin
Application.HandleException(Self);
Result := True;
end;
All my forms inherit from TMyForm. I plan to override this function to solve the problem:
function TMyForm.HandleCreateException: Boolean;
begin
Result := False;
end;
But I cannot imagine anybody wanting an exception on form creation to be swallowed, leaving a potentially halfly initialized form, yet this code still exists in VCL. This leaves me with the question:
Are there any reasons to not treat unhandled exceptions on form creation this way? Or are there better options to handle my problem?
Edited to clarify the original problem. I have the following code:
try
//...
Frm := TBooForm.Create(...);
Frm.ShowModal;
//...
except
//exception in TBooForm.FormCreate not landing here.
end;
If I don't touch global exception handling, an unhandled exception in TBooForm.FormCreate() will not land in the exception block. Instead Frm will display in halfly initialized state, leading to hard to track errors.

ADODataSet.Open bypasses try catch with `ArgumentOutOfRange` exception, hangs application - Delphi 10.2

I maintain an application that runs as a service in a server environment. It is multithreaded, where each thread does work according to a task queue. This task queue is just a list of strings with "job types" as their values. So while several threads may be running, each thread would be a different job, and each thread internally runs just one task at a time.
I'm experiencing an intermittent issue that happens when calling Open on a TADODataSet. Sometimes, not always, and with no discernible pattern, Data.Win.ADODB will throw an EArgumentOutOfRangeException, bypassing my own attempt to catch any exceptions. This exception hangs the entire thread and prevents future execution from being possible until I completely restart the service.
Being relatively new to the world of Delphi, I've been scratching my head at this issue for quite some time, and have struggled to find any answers. My question is: why is this happening, and how do I stop, catch, or fix it?
Here is a snippet of my offending code. This is the method from which the stack trace originates. It is called from another method in the same unit, where I open a different dataset, loop through its records, and on each record call this function to get some info based on the value passed in.
function TFQFoo.DoSomething(IncNo : Int64): string;
var
ItemList : string;
MySQL: string;
ComponentString: string;
begin
result:='';
if IncNo<=0 then
Exit;
ItemList := '';
MyQuery.Close;
MySQL := 'select ID from tbl ' +
' where val = ' + IntToStr(IncNo) +
' order by col1 DESC, col2, col3';
try
try
MyQuery.CommandText := (MySQL);
MyQuery.Open;
while not (MyQuery.EOF) do
begin
if (ItemList <> '') then
ItemList := ItemList + ',';
ItemList := ItemList +
MyQuery.FieldbyName('ID').asstring;
MyQuery.Next;
end;
except
// exception handling code omitted for brevity -- none of it
// is ever reached, anyway (see below)
end;
finally
MyQuery.Close;
end;
Result := ItemList;
end;
The call stack from the exception indicates that it's occurring at Open. No try..catch block will capture the exception and log it for me -- I have to use EurekaLog in order to see any details. The stack trace methods (too big to post here) look like:
TFQFoo.DoSomething
TDataSet.Open
... internal things
TADOConnection.ExecuteComplete
CheckForAsyncExecute
...
TCustomConnection.GetDataSet
TListHelper.GetItemRange
Thinking possibly my TADODataSet component was somehow getting corrupted / its properties altered at runtime, I added some logging to capture that data for me so I could see if something funky was going on there. I didn't see anything, but here it is in case it's pertinent.
object MyQuery: TADODataSet
AutoCalcFields = False
CacheSize = 15
Connection = FGlobals.RIMSDB
CursorType = ctStatic
LockType = ltReadOnly
CommandText =
'select ID from tbl where val = 202005070074 order by col1 ' +
'DESC, col2, col3'
ParamCheck = False
Parameters = <>
Left = 32
Top = 216
end
For the curious, this is the method actually throwing the exception, from Data.Win.ADODB. Note the except, which I guess hops over my own try..catch block and sends the exception straight to EurekaLog.
procedure CheckForAsyncExecute;
var
I: Integer;
begin
try
if not Assigned(pError) and Assigned(pRecordset) and
((pRecordset.State and adStateOpen) <> 0) then
for I := 0 to DataSetCount - 1 do
if (DataSets[I].Recordset = pRecordset) and (eoAsyncExecute in DataSets[I].ExecuteOptions) then
begin
DataSets[I].OpenCursorComplete;
Break;
end;
except
ApplicationHandleException(Self);
end;
end;
What I have tried:
Many, many iterations of tweaking component properties on the ADODataSet itself
Using the CommandText and Parameters from within the designer, and assigning the parameter prior to execution at runtime
Adding / removing a (NOLOCK) hint to the query itself
Taken the problem to senior members of my team for input
Googling (for hours)
Reading up on Delphi and ADO documentation (not fruitful for this)
Attempted reproduction - I've never been able to get this to occur on any test system I use. This leads me to think it may be environment-related, but I have absolutely no clue how
My question, restated:
How do I stop this from happening? What am I doing wrong? I don't want to just catch the EArgumentOutOfRangeException; I want to learn why it's happening in the first place and prevent it from happening in the future.
I know that sometimes, the query execution will not return results, but the typical CommandText does not return a result set message is never seen nor encountered due to the lower-level code bypassing my own catch statement. Beyond this, I don't know what else to look for.
I've only found one other occurrence of something similar so far, but it relates just to the exception not getting caught: http://www.delphigroups.info/2/d9/410191.html
The call to ApplicationHandleException(Self) in CheckForAsyncExecute() is swallowing exceptions, which is why your except block is not being triggered:
// in Data.Win.ADODB.pas:
procedure CheckForAsyncExecute;
var
I: Integer;
begin
try
...
except
ApplicationHandleException(Self); // <-- a caught exception is NOT re-raised here!
end;
end;
Inside of the Data.Win.ADODB unit, caught exceptions will call the unit's own ApplicationHandleException() function, which then calls System.Classes.ApplicationHandleException if assigned, otherwise it simply exits:
// in Data.Win.ADODB.pas:
procedure ApplicationHandleException(Sender: TObject);
begin
if Assigned(System.Classes.ApplicationHandleException) then
System.Classes.ApplicationHandleException(Sender);
end;
System.Classes.ApplicationHandleException is initialized to nil.
In both a VCL1 and FMX app, the TApplication constructor assigns the TApplication.HandleException() method to System.Classes.ApplicationHandleException, where HandleException() ignores EAbort exceptions, and calls the TApplication.OnException event handler (if assigned), the TApplication.ShowException() method, or the System.SyUtils.ShowException() function, depending on the type of exception being handled.
1: in a VCL TService app, TServiceApplication uses Vcl.Forms.TApplication internally.
TApplication.ShowException() displays the details of the exception to the user in a popup MessageBox and then exits, and System.SysUtils.ShowException() displays the details of the exception to the user in a Console or MessageBox and then exits.
So, at no point does ADO's CheckForAsyncExecute() re-raise a caught exception into user code. And needless to say, displaying a popup MessageBox in a service is not a good idea, as the user will likely not see it so they can dismiss it.
Of course, the best option would be to avoid the EArgumentOutOfRangeException exception from being raised in the first place. But there are other conditions that can also raise exceptions, too.
So, your only option to handle swallowed ADO exceptions yourself, and avoid popup MessageBoxes, is to assign a TApplication.OnException event handler (either directly, or via the TApplicationEvents component).

Get exception text from FireDAC debugger notification

I'm trying to capture the debugger notification text, Firedac connection
//FDScript1.SQLScripts.Add.SQL.LoadFromFile('C:\SGI\Rodar_Apos_Atualizar_3.txt');
//FDScript1.ValidateAll;
//FDScript1.ExecuteAll;
//MSScript1.SQL.LoadFromFile('C:\SGI\Rodar_Apos_Atualizar_3.txt');
//MSScript1.Execute;
try
FDConnection1.Connected := True;
FDScript1.SQLScripts.Add.SQL.LoadFromFile('C:\SGI\Rodar_Apos_Atualizar_3.txt');
FDScript1.ExecuteAll;
FDScript1.ValidateAll;
except
//EMSSQLNativeException
on E: EMSSQLNativeException do
begin
//ShowMessage('Erro'+FDGUIxErrorDialog1.ErrorDialog.Caption);
//Memo1.Clear;
Memo1.Text := 'Erro de Sistema: '+#13#10+ E.Message;
end;
end;
It really would help if you would show us the scripts you are trying to execute,
or at least the script which does not execute correctly. In any case, your code is wrong
because the documentation
states
It is good practice to call the ValidateAll method before the ExecuteAll method.
Note the 'before'. Your ValidateAll is after ExecuteAll, not before. Both are Boolean
functions but you are not checking their results, which you should.
With some trivial experimenting I found that I can provoke a EMSSQLNativeException
using SqlServer 2014 with the code below:
procedure TForm2.Button1Click(Sender: TObject);
const
sScript = 'select m.*, d.* from master m join detail d on, m.masterid = d.masterid';
var
FDScript : TFDSqlScript;
begin
try
FDConnection1.Connected := True;
FDScript := FDScript1.SQLScripts.Add;
FDScript.SQL.Text := sScript;
//FDScript1.ExecuteAll;
//FDScript1.ValidateAll;
// FDScript1.ValidateAll then
//FDScript1.ExecuteAll;
FDQuery1.SQL.Text := sScript;
FDQuery1.Open();
except
//EMSSQLNativeException
on E: EMSSQLNativeException do
begin
//ShowMessage('Erro'+FDGUIxErrorDialog1.ErrorDialog.Caption);
//Memo1.Clear;
Memo1.Text := 'Erro de Sistema: '+#13#10+ E.Message;
end;
end;
end;
Note the blatantly wrong syntax in the Sql statement, namely the comma after the on.
When FDQuery1.Open is called, this exception is raised (and caught initially by the debugger)
---------------------------
Debugger Exception Notification
---------------------------
Project sqlerror.exe raised exception class EMSSQLNativeException with message '[FireDAC][Phys][ODBC][Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near 'd'.'.
---------------------------
Break Continue Help
---------------------------
When I click Continue, execution proceeds into your exception handler, exactly as #TomBrunberg described in a comment
and the exception's message text is inserted into Memo1.
So I cannot reproduce the behaviour you describe based on the information in your question. It
must be caused by something you are not telling us, possibly in code you have
not included in your q or some property setting of the components you are
using.
Hopefully, trying the code above, you will find the debugger behaving as I
have described and this may give you some clue as to why you are getting the
problem you've described. Note that it is very important that you try the code
above in a new project, not your existing one. The only property setting
you need to do before executing it is to set FDConnection1 so that it can connect
to your server.
FWIW, if I uncomment-out the lines
FDScript1.ValidateAll then
FDScript1.ExecuteAll;
they execute without complaint, and I get the exact same behaviour as i've described without them.

FireDAC TFDScript error trying to drop a non-existent table

I have a nasty error when executing a Firedac TFDScript error trying to drop a non-existent table:
Delphi Berlin 10.1 Upd 2
Database Firebird 2.5
It give an error when calling FDScript.ExecuteAll (it passes the FDScript.ValidateAll without any problem)
The code I am executing is as follows:
FDScript: TFDScript;
{...}
begin
FDScript.ScriptOptions.Reset;
FDScript.SQLScripts.Clear;
FDScript.SQLScriptFileName := '';
FDScript.ScriptOptions.CommandSeparator := ';';
FDScript.ScriptOptions.CommitEachNCommands := 1;
FDScript.ScriptOptions.DropNonexistObj := True; // seems to ignore this directive
FDConnection.StartTransaction;
try
FDScript.SQLScripts.Add.SQL.Add('drop table countries;');
FDScript.ValidateAll; // no errors here
ScriptStatus := GetEnumName(TypeInfo(TFDScriptStatus), Ord(FDScript.Status));
if FDScript.Status = ssFinishSuccess then begin
FDScript.ExecuteAll; // ERROR HERE! TABLE COUNTRIES DOES NOT EXIXTS
if FDScript.TotalErrors = 0 then begin
FDConnection.Commit;
end
else begin
FDConnection.Rollback;
end;
end
else begin
FDConnection.Rollback;
end;
except
FDConnection.Rollback;
raise;
end;
end;
Implementation seems to be correct. The engine checks if a raised exception is of ekObjNotExists kind, and if so and the DropNonexistObj property is enabled and command kind is DROP or ALTER, it logs to its console. Otherwise re-raises the caught exception.
The only explanation is then, that you are seeing an exception message dialog shown by debugger. These dialogs are displayed even for handled exceptions (so long you won't add them to an ignore list or turn this feature off, which you should not do).

Exception is generated twice by TNMPOP3.Connect

Long time ago I wrote following code to retrieve emails from mailbox:
pop3 := TNMPOP3.Create(Self);
try
pop3.Host := FAppSettings.ServerName;
pop3.Port := FAppSettings.ServerPort;
pop3.UserID := FAppSettings.Login;
pop3.Password := FAppSettings.Password;
try
pop3.Connect;
except
on E:Exception do AddError(E.Message);
end;
if not pop3.Connected then Exit;
if pop3.MailCount > 0 then begin
pop3.DeleteOnRead := False;
pop3.AttachFilePath := GetTempDirectory;
ProcessMsgs(pop3);
end
else begin
TCommon.InfMsg('There are no messages in mailbox');
end;
pop3.Disconnect;
finally
pop3.Free;
end;
Now, when mail service provider switched entirely to SSL this code fails obviously, but in a strange way:
pop3.Connect line causes an exception but with an empty text in E.Message making the problem unclear to end user.
Investigation of the problem in Delphi debugger reveals that the first time the right exception is generated:
Project .... raised exception class Exception with message 'Authentication failed'.
but then, when I press F8 (Step Over) again, execution point remains in the same line and another exception is generated:
Project .... raised exception class Exception with message ''.
and only this exception is catched by try-except.
Why?
To answer your actual question of "why?", the sequence you describe means that TNMPOP3.Connect() is internally catching the original authentication exception and throwing a new exception without an error message. Whether that is a bug or intentional, there is no way to know without looking at the source code for TNMPOP3. Delphi did not ship with that source code, and NetMasters are no longer around so you can't ask them for it. TNMPOP3 does not support SSL anyway, so you will have to switch to another component/library to handle your POP3+SSL functionality moving forward.

Resources