Raising Exception in TThread Execute?
Asked Answered
S

6

9

I just realized that my exceptions are not being shown to the user in my threads!

At first I used this in my thread for raising the exception, which does not work:

except on E:Exception do
begin
  raise Exception.Create('Error: ' + E.Message);
end;

The IDE shows me the exceptions, but my app does not!

I have looked around for a solution, this is what I found:

Delphi thread exception mechanism

http://www.experts-exchange.com/Programming/Languages/Pascal/Delphi/Q_22039681.html

And neither of these worked for me.

Here's my Thread unit:

unit uCheckForUpdateThread;

interface

uses
  Windows, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient,
  IdHTTP, GlobalFuncs, Classes, HtmlExtractor, SysUtils, Forms;

type
  TUpdaterThread = class(TThread)
  private
    FileGrabber : THtmlExtractor;
    HTTP : TIdHttp;
    AppMajor,
    AppMinor,
    AppRelease : Integer;
    UpdateText : string;
    VersionStr : string;
    ExceptionText : string;
    FException: Exception;
    procedure DoHandleException;
    procedure SyncUpdateLbl;
    procedure SyncFinalize;
  public
    constructor Create;

  protected
    procedure HandleException; virtual;

    procedure Execute; override;
  end;

implementation

uses
  uMain;

{ TUpdaterThread }

constructor TUpdaterThread.Create;
begin
  inherited Create(False);
end;

procedure TUpdaterThread.Execute;
begin
  inherited;
  FreeOnTerminate := True;

  if Terminated then
    Exit;

  FileGrabber           := THtmlExtractor.Create;
  HTTP                  := TIdHTTP.Create(nil);
  try
    try
      FileGrabber.Grab('http://jeffijoe.com/xSky/Updates/CheckForUpdates.php');
    except on E: Exception do
    begin
      UpdateText := 'Error while updating xSky!';
      ExceptionText := 'Error: Cannot find remote file! Please restart xSky and try again! Also, make sure you are connected to the Internet, and that your Firewall is not blocking xSky!';
      HandleException;
    end;
    end;

    try
      AppMajor      := StrToInt(FileGrabber.ExtractValue('AppMajor[', ']'));
      AppMinor      := StrToInt(FileGrabber.ExtractValue('AppMinor[', ']'));
      AppRelease    := StrToInt(FileGrabber.ExtractValue('AppRelease[[', ']'));
    except on E:Exception do
    begin
      HandleException;
    end;
    end;

    if (APP_VER_MAJOR < AppMajor) or (APP_VER_MINOR < AppMinor) or (APP_VER_RELEASE < AppRelease) then
    begin
      VersionStr := Format('%d.%d.%d', [AppMajor, AppMinor, AppRelease]);
      UpdateText := 'Downloading Version ' + VersionStr;
      Synchronize(SyncUpdateLbl);
    end;

  finally
    FileGrabber.Free;
    HTTP.Free;
  end;
  Synchronize(SyncFinalize);
end;

procedure TUpdaterThread.SyncFinalize;
begin
  DoTransition(frmMain.TransSearcher3, frmMain.gbLogin, True, 500);
end;

procedure TUpdaterThread.SyncUpdateLbl;
begin
  frmMain.lblCheckingForUpdates.Caption := UpdateText;
end;

procedure TUpdaterThread.HandleException;
begin
  FException := Exception(ExceptObject);
  try
    Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

procedure TUpdaterThread.DoHandleException;
begin
  Application.ShowException(FException);
end;

end.

If you need more info just let me know.

Again: The IDE catches all the exceptions, but my program does not show them.

EDIT: It was Cosmin's solution that worked in the end - and the reason it didn't at first, was because I didn't add the ErrMsg variable, instead I just placed whatever the variable would contain into the Synchronize, which would NOT work, however I have NO idea why. I realized it when I had no other ideas, and I just messed around with the solutions.

As always, the joke's on me. =P

Shih answered 26/3, 2011 at 14:36 Comment(16)
Could you post your sorce plz?Mauchi
I removed the Raise from the code, since it did not work. I tried with using a Synchronized raise, too, which did not work - thats why the ExceptionText is there, forgot to remove it.Shih
Perhaps you are not having exceptions at all? What kind of exceptions are you having?Mauchi
@Rafael - Exceptions caused by the TIdHTTP control, because I know the file on the webserver does not exist. I was testing if the exception logic did actually work, and I was shocked when I realized it didn't.Shih
By "not being raised" you mean "no error message box is shown to the user"?Authenticity
Whoever it was, please tell me why you voted down, so I know what I can do to better help you guys help me.Shih
As others have tried to tell you already, exceptions in secondary threads are not shown to the user, you have to add code to do that yourself.Villous
@Lasse - And I tried that, but that would not work either, they are still not shown! I tried the code that has been posted, and it still won't show it. Only the IDE catches the exception.Shih
If you set a breakpoint in your HandleException method, does it stop there?Villous
@Lasse - Only the second one does, even though the IDE raises the first one, too - that is, that I set the breakpoint on the HandleException; method in my Execute procedure. The second exception is being shown my app, but not the first one - the one that raises an EIdHTTPProtocolException.Shih
I'm guesing someone voted you down because this (as worded) isn't really a question. You could be asking "why don't I see exceptions raised in threads" or "how do I show an exception from a TThread to the user". The ambiguity makes it more difficult to answer. I've provided a full answer to the first question, and a briefer answer to the second which is largely dependent on what you want to achieve.Hysterics
@Craig - **You could be asking "why don't I see exceptions raised in threads" or "how do I show an exception from a TThread to the user". ** - I don't see where the difference is, to be honest. :)Shih
Jeff, if you can't tell the difference between "exceptions aren't shown to the user" and "exceptions aren't being raised," then you also can't tell the difference between "I've been stuck indoors all day" and "the sun didn't rise today." Just because you're not notified of something doesn't mean it didn't happen. Please edit your question to be more precise about exactly what did or didn't happen, and what your expectations were.Somite
@Rob, I already did edit it - I dont say "exceptions aren't being raised," anymore.Shih
@Jeff, are you sure? Read the first sentence of your question.Hibernia
@Cosmin - Wups, was focusing around the middle - jokes on me eh? :PShih
H
9

Here's my very, very short "take" on the issue. It only works on Delphi 2010+ (because that version introduced Anonymous methods). Unlike the more sophisticated methods already posted mine only shows the error message, nothing more, nothing less.

procedure TErrThread.Execute;
var ErrMsg: string;
begin
  try
    raise Exception.Create('Demonstration purposes exception');
  except on E:Exception do
    begin
      ErrMsg := E.ClassName + ' with message ' + E.Message;
      // The following could be all written on a single line to be more copy-paste friendly  
      Synchronize(
        procedure
        begin
          ShowMessage(ErrMsg);
        end
      );
    end;
  end;
end;
Hibernia answered 26/3, 2011 at 16:41 Comment(11)
Just as with the others, only the IDE shows the error, the application does not raise the exception :(Shih
@David - works absolutely perfect, it stops execution and everything!Shih
@David - that is, if I do it outside a thread, so no synchronize or anything TThread relatedShih
@jeff kind of hard to tell. Must be something we can't see.Salas
@David - It appears I can raise an exception using Synchronize, but ONLY OUTSIDE the Except-End block (that means I can do it between Try, and Except). Why is that? However it did not stop execution.Shih
@Jeff, copy-paste my code as is in a new Forms application, tell us if it shows you the error. That code works, I'm using it in production, and I also tested it before posting. And it's only doing ShowMessage in Synchronize, I'm honestly having a really hard time believing it doesn't work.Hibernia
@Cosmin - A new project works fine. Will edit my OP to show you the code where it did NOT work (and STILL does not work)Shih
@Cosmin - it appears your solution worked afterall. :) Thanks!Shih
I cant understand why does this code work and the one I posted dont. The only difference here is the anonymous method, and thats not what cause your code to work.Mauchi
@Rafael, one minor difference is that you're passing the whole exception object into the VCL thread, and I found that to be unreliable. Combine what you've got with David's AcquireExceptionObject method and it's better. For what it's worth I upvoted your answer, as well as one other, because I believe they're good answers. Whatever error there was, it was in Jeff's code.Hibernia
@Cosmin, ok .. got it. But the source I posted here was just a example. I never pass the whole exception into the VCL thread, but i thought it was the easier thing to show. Actually i dont even work with exceptions that way. And also I thought it would be easy to adapt the code to pass a string or any other object, since I was just demonstrating that you need to notify the vcl thread when an exception occurs and also you need to call synchronize.Mauchi
H
14

Something very important you need to understand about multi-theraded development:

Each thread has its own call-stack, almost as if they're separate programs. This includes the main-thread of your program.

Threads can only interact with each other in specific ways:

  • They can operate on shared data or objects. This can lead to concurrency issues 'race conditions', and therefore you need to be able to help them 'share data nicely'. Which brings us to the next point.
  • They can "signal each other" using a variety of OS support routines. These include things like:
    • Mutexes
    • Critical Sections
    • Events
  • And finally you can send messages to other threads. Provided the thread has in some way been written to be a message receiver.

NB: Note that threads cannot strictly speaking call other threads directly. If for example Thread A tried to call Thread B directly, that would be a step on Thread A's call-stack!

This brings us to the topic of the question: "exceptions are not being raised in my threads"

The reason for this is that all an exception does is:

  • Record the error
  • And unwind the call-stack. <-- NB: Your TThread instance can't unwind the main thread's call-stack, and cannot arbitrarily interrupt the main threads execution.

So TThread will not automatically report exceptions to your main application.

You have to make the explicit decision as to how you wish to handle errors in threads, and implement accordingly.

Solution

  • The first step is the same as within a single threaded application. You need to decide what the error means and how the thread should react.
    • Should the thread continue processing?
    • Should the thread abort?
    • Should the error be logged/reported?
    • Does the error need a user decision? <-- This is by far the most difficult to implement, so we'll skip it for now.
  • Once this has been decided, implement the appropriate excpetion handler.
  • TIP: Make sure the exception doesn't escape the thread. The OS won't like you if it does.
  • If you need the main program (thread) to report the error to the user, you have a few options.
    • If the thread was written to return a result object, then it's easy: Make a change so that it can return the error in that object if something went wrong.
    • Send a message to the main thread to report the error. Note, the main thread already implements a message loop, so your application will report the error as soon as it processes that message.

EDIT: Code Sample for indicated requirement.

If all you want to do is notify the user, then Cosmind Prund's answer should work perfectly for Delphi 2010. Older versions of Delphi need a little more work. The following is conceptually similar to Jeff's own answer, but without the mistakes:

procedure TUpdaterThread.ShowException;
begin
  MessageDlg(FExceptionMessage, mtError, [mbOk], 0);
end;

procedure TUpdaterThread.Execute;
begin
  try

    raise Exception.Create('Test Exception');
    //The code for your thread goes here
    //
    //

  except
    //Based on your requirement, the except block should be the outer-most block of your code
    on E: Exception do
    begin
      FExceptionMessage := 'Exception: '+E.ClassName+'. '+E.Message;
      Synchronize(ShowException);
    end;
  end;
end;

Some important corrections on Jeff's own answer, including the implementation shown within his question:

The call to Terminate is only relevant if your thread is implemented within a while not Terminated do ... loop. Take a look at what the Terminate method actually does.

The call to Exit is an unnecessary waste, but you probably did this because of your next mistake.

In your question, you're wrapping each step in its own try...except to handle the exception. This is an absolute no-no! By doing this you pretend that even though an exception occurred, everything is ok. Your thread tries the next step, but is actually guaranteed to fail! This is not the way to handle exceptions!

Hysterics answered 26/3, 2011 at 17:27 Comment(3)
Thank you for the information, I did not know the fact about the Callstack! I have already decided what I want my thread to do when an exception occurs - I want it to terminate, and let the user know what went wrong, and give them the Exception.Message, too. The thread does not return anything (I didnt even know it could that, so I could make like a function out of it??).Shih
Jeff, the only time thread execution stops is when the thread terminates. Reaching an except block does not cause termination. The debugger can interrupt thread execution, but that's not the same as terminating, and besides the debugged application has no direct knowledge of a debugger's actions on it. You can also suspend execution by sleeping, waiting, or sending a message to another thread. Those aren't the same as terminating, either.Somite
@Jeff: In a manner of speaking your thread is already returning results. It's updating a label on your main form. As for execution stopping on an exception: if you handle the exception inside a while not Terminated loop, then it will continue to execute. However, if you don't handle the exception, it unwinds your call-stack back to the thread's entry point and terminates. However, most OS's faced with this "bad behaviour" will GPF your entire application.Hysterics
H
9

Here's my very, very short "take" on the issue. It only works on Delphi 2010+ (because that version introduced Anonymous methods). Unlike the more sophisticated methods already posted mine only shows the error message, nothing more, nothing less.

procedure TErrThread.Execute;
var ErrMsg: string;
begin
  try
    raise Exception.Create('Demonstration purposes exception');
  except on E:Exception do
    begin
      ErrMsg := E.ClassName + ' with message ' + E.Message;
      // The following could be all written on a single line to be more copy-paste friendly  
      Synchronize(
        procedure
        begin
          ShowMessage(ErrMsg);
        end
      );
    end;
  end;
end;
Hibernia answered 26/3, 2011 at 16:41 Comment(11)
Just as with the others, only the IDE shows the error, the application does not raise the exception :(Shih
@David - works absolutely perfect, it stops execution and everything!Shih
@David - that is, if I do it outside a thread, so no synchronize or anything TThread relatedShih
@jeff kind of hard to tell. Must be something we can't see.Salas
@David - It appears I can raise an exception using Synchronize, but ONLY OUTSIDE the Except-End block (that means I can do it between Try, and Except). Why is that? However it did not stop execution.Shih
@Jeff, copy-paste my code as is in a new Forms application, tell us if it shows you the error. That code works, I'm using it in production, and I also tested it before posting. And it's only doing ShowMessage in Synchronize, I'm honestly having a really hard time believing it doesn't work.Hibernia
@Cosmin - A new project works fine. Will edit my OP to show you the code where it did NOT work (and STILL does not work)Shih
@Cosmin - it appears your solution worked afterall. :) Thanks!Shih
I cant understand why does this code work and the one I posted dont. The only difference here is the anonymous method, and thats not what cause your code to work.Mauchi
@Rafael, one minor difference is that you're passing the whole exception object into the VCL thread, and I found that to be unreliable. Combine what you've got with David's AcquireExceptionObject method and it's better. For what it's worth I upvoted your answer, as well as one other, because I believe they're good answers. Whatever error there was, it was in Jeff's code.Hibernia
@Cosmin, ok .. got it. But the source I posted here was just a example. I never pass the whole exception into the VCL thread, but i thought it was the easier thing to show. Actually i dont even work with exceptions that way. And also I thought it would be easy to adapt the code to pass a string or any other object, since I was just demonstrating that you need to notify the vcl thread when an exception occurs and also you need to call synchronize.Mauchi
S
6

Threads don't automatically propagate exceptions into other threads. So you must deal with it yourself.

Rafael has outlined one approach, but there are alternatives. The solution Rafael points to deals with the exception synchronously by marshalling it into the main thread.

In one of my own uses of threading, a thread pool, the threads catch and take over the ownership of the exceptions. This allows the controlling thread to handle them as it pleases.

The code looks like this.

procedure TMyThread.Execute;
begin
  Try
    DoStuff;
  Except
    on Exception do begin
      FExceptAddr := ExceptAddr;
      FException := AcquireExceptionObject;
      //FBugReport := GetBugReportCallStackEtcFromMadExceptOrSimilar.
    end;
  End;
end;

If the controlling thread elects to raise the exception it can do so like this:

raise Thread.FException at Thread.FExceptAddr;

Sometimes you may have code that cannot call Synchronize, e.g. some DLLs and this approach is useful.

Note that if you don't raise the exception that was captured, then it needs to be destroyed otherwise you have a memory leak.

Salas answered 26/3, 2011 at 15:0 Comment(7)
@David - what should I say then? I post the info I haveShih
@Jeff: just bear in mind that we are not psychic nor clair-voyant, we don't know your code base, we have no way of seeing what's on your screen, we don't know what steps you took, what results you expected and what results you got. If you want good answers, you'll need to supply all of that for us to even have a chance of helping you. Otherwise all we can do is guess at what the problem might be and how you might solve that...Teofilateosinte
@Jeff: It's a lot to read, but will help you enormously in asking questions the smart way: catb.org/~esr/faqs/smart-questions.htmlTeofilateosinte
@Marjan - I believe I did post the steps I took, and the results I got. But thanks, will keep that in mind.Shih
David, congrats on the gold Delphi badge. And +1 for AcquireExceptionObject, because I learned something new!Hibernia
@jeff read the first comment to this answer and put yourself in our shoes. Anyway it sounds like Synchronize might be right for your needs.Salas
@David - Yeah, I see what you mean, however the fault was the same: Only the IDE tells me theres an exception. Also, using Synchronize (as the anwers suggest) does not raise an exception in my app either. I don't know why it's doing this to me.. I would like to be able to show a custom message, followed by the actual Exception.Message, just like you would do in a regular TryExcept block. Synchronize should do the job though, shouldnt it? I tried raising the exception by using Synchronize, so since it's being executed in the main thread, it should work?Shih
M
4

Well,

It is gonna be hard without your source code, but i have tested this:

How to handle exceptions in TThread objects

And it works fine. Perhaps you should take a look at it.

EDIT:

You are not following what the links you point out tell us to do. Check my link and you will see how to do that.

EDIT 2:

Try that and tell me if it worked:

 TUpdaterThread= class(TThread)
 private
   FException: Exception;
   procedure DoHandleException;
 protected
   procedure Execute; override;
   procedure HandleException; virtual;
 end;

procedure TUpdaterThread.Execute;
begin
  inherited;
  FreeOnTerminate := True;
  if Terminated then
    Exit;
  FileGrabber := THtmlExtractor.Create;
  HTTP := TIdHTTP.Create(Nil);
  try
    Try
      FileGrabber.Grab('http://jeffijoe.com/xSky/Updates/CheckForUpdates.php');
    Except
      HandleException;
    End;
    Try
      AppMajor := StrToInt(FileGrabber.ExtractValue('AppMajor[', ']'));
      AppMinor := StrToInt(FileGrabber.ExtractValue('AppMinor[', ']'));
      AppRelease := StrToInt(FileGrabber.ExtractValue('AppRelease[[', ']'));
    Except
      HandleException;
    End;
    if (APP_VER_MAJOR < AppMajor) or (APP_VER_MINOR < AppMinor) or (APP_VER_RELEASE < AppRelease) then begin
      VersionStr := Format('%d.%d.%d', [AppMajor, AppMinor, AppRelease]);
      UpdateText := 'Downloading Version ' + VersionStr;
      Synchronize(SyncUpdateLbl);
    end;
  finally
    FileGrabber.Free;
    HTTP.Free;
  end;
  Synchronize(SyncFinalize);

end;

procedure TUpdaterThread.HandleException;
begin
  FException := Exception(ExceptObject);
  try
    Synchronize(DoHandleException);
  finally
    FException := nil;
  end;
end;

procedure TMyThread.DoHandleException;
begin
  Application.ShowException(FException);
end;

EDIT 3:

You said you are no able to catch EIdHTTPProtocolException. But it works for me. Try this sample and see it for yourself:

procedure TUpdaterThread.Execute;
begin
  Try
    raise EIdHTTPProtocolException.Create('test');
  Except
    HandleException;
  End;
end;
Mauchi answered 26/3, 2011 at 14:49 Comment(11)
I tried that one - it's basically the same code as in the SO question I linked to.Shih
@Rafael - I removed the Raise ones because it did not work, and I was trying out the other methods, etc. Trust me, I tried the methods.Shih
well .. then i really dont know because i tried this code and it works fine here.Mauchi
Did exactly what your code says, and it did not work sadly :( In fact, the IDE catches the exception, but the program does not even stop, it just continues (if I press continue, but the REAL exception is not shown to the user)Shih
@Rafael - turns out it only checks for class Exception, and not stuff like EIdHTTPProtocolException - can I make it catch all exceptions? Also, how do I assign my custom message to it? Oh, and the program does not stop execution :SShih
It does handle EIdHTTPProtocolException. I have changed the source to raise EIdHTTPProtocolException and it works also. The program does not stop execuction because you are working with threads. If you wanna you program to stop on a thread exception you should signalize the thread error to the main thread app and check this signal on the main thread aborting the program execution.Mauchi
Perhaps you should close this question and create another one asking "how to stop a program execution when a thread raise a exception".Mauchi
"how do I assign my custom message to it"? you just have to re-raise the exception like raise Exception.Create('My error: ' + E.message)Mauchi
I cant help you anymore .. thats all i know about your source code. Maybe you have another code that does not allow the message to be shown.Mauchi
@Rafael - If I re-raise the exception in the DoHandleException, it raises nothing outside the IDEShih
As I already told you, your code works fine to me. Even when a re-raise the exception it works, so it must be a localized problem.Mauchi
E
2

I've previously used SendMessge for inter thread communication using the TWMCopyData, so I think the following should work:

Const MyAppThreadError = WM_APP + 1;

constructor TUpdaterThread.Create(ErrorRecieverHandle: THandle);
begin
    Inherited Create(False);
    FErrorRecieverHandle := Application.Handle;
end;

procedure TUpdaterThread.Execute;
var
    cds: TWMCopyData;
begin
  try
     DoStuff;
  except on E:Exception do
    begin
        cds.dwData := 0;
        cds.cbData := Length(E.message) * SizeOf(Char);
        cds.lpData := Pointer(@E.message[1]);         
        SendMessage(FErrorRecieverHandle, MyAppThreadError, LPARAM(@cds), 0);
    end;
  end;
end;

I've only used it for sending simple data types or strings, but I'm sure it could be adapted send more information through as necessary.

You'll need add Self.Handle to the constructor in form created the thread and Handle the messsage in the form which created it

procedure HandleUpdateError(var Message:TMessage); message MyAppThreadError;
var
    StringValue: string;
    CopyData : TWMCopyData; 
begin
    CopyData := TWMCopyData(Msg);
    SetLength(StringValue, CopyData.CopyDataStruct.cbData div SizeOf(Char));
    Move(CopyData.CopyDataStruct.lpData^, StringValue[1], CopyData.CopyDataStruct.cbData);
    Message.Result := 0;
    ShowMessage(StringValue);
end;
Electrotype answered 26/3, 2011 at 17:20 Comment(0)
F
1

Strange that everyone answered this question but failed to spot the obvious problem: given that exceptions raised in a background thread are asynchronous, and can occur at any time, this means that showing exceptions from a background thread would pop-up a dialog box at random times to the user, quite possibly showing an exception that has nothing to do with what the user is doing at the moment. I doubt that doing this could possibly enhance the user experience.

Fagen answered 26/3, 2011 at 23:4 Comment(5)
Who says that it's unrelated to what the user is doing? Not everything that lives in a thread is so.Salas
True, but given the asynchronous nature of thread-based work, how would you know? Surely part of the process here is to not only blindly respond to user's questions, but to suggest sometimes that the user is asking the wrong question? In this case maybe showing errors in a form/status bar could be what is really required, but popping up dialogs initiated by asynchronous thread-based processing? Would anyone really want to do this to a user?Fagen
@Fagen All good points, but this question of Jeffs is part of a long-running series. As I understand it he's using threads in response to user action to communicate through some Skype API. And he uses threads because if he did it in the main thread then his UI would go non-responsive. So you are quite right to question the wisdom of showing a dialog, but I think in this instance Jeff is probably doing it right.Salas
We did not explain that to him because that was not the question he asked. He needs help with his thread, not a explanation of how show errors to user.Mauchi
Although not specific to the question, this is an excellent general comment on the subject of errors. Even in single threaded environments, error dialogs can be quite instrusive on the user experience - and users tend not to read them in any case. (Just take a look at how many SO questions state: I got an error, with giving any hint as to the message itself.) In some cases it may be more practical to add the error to a "Messages Window", or dynamically show a button/icon indicating that errors have occurred.Hysterics

© 2022 - 2024 — McMap. All rights reserved.