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

Community
  • 1
  • 1
Jeff
  • 12,085
  • 12
  • 82
  • 152
  • Could you post your sorce plz? – Rafael Colucci Mar 26 '11 at 14:46
  • 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. – Jeff Mar 26 '11 at 14:54
  • Perhaps you are not having exceptions at all? What kind of exceptions are you having? – Rafael Colucci Mar 26 '11 at 15:09
  • @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. – Jeff Mar 26 '11 at 15:11
  • By "not being raised" you mean "no error message box is shown to the user"? – CodesInChaos Mar 26 '11 at 15:25
  • Whoever it was, please tell me why you voted down, so I know what I can do to better help you guys help me. – Jeff Mar 26 '11 at 16:06
  • 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. – Lasse V. Karlsen Mar 26 '11 at 16:06
  • @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. – Jeff Mar 26 '11 at 16:16
  • If you set a breakpoint in your HandleException method, does it stop there? – Lasse V. Karlsen Mar 26 '11 at 16:28
  • @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. – Jeff Mar 26 '11 at 16:39
  • 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. – Disillusioned Mar 26 '11 at 17:32
  • @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. :) – Jeff Mar 26 '11 at 18:03
  • 2
    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. – Rob Kennedy Mar 26 '11 at 19:05
  • @Rob, I already did edit it - I dont say "exceptions aren't being raised," anymore. – Jeff Mar 26 '11 at 19:22
  • @Jeff, are you sure? Read the *first sentence* of your question. – Cosmin Prund Mar 26 '11 at 19:49
  • @Cosmin - Wups, was focusing around the middle - jokes on me eh? :P – Jeff Mar 26 '11 at 20:07

6 Answers6

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!

Community
  • 1
  • 1
Disillusioned
  • 14,635
  • 3
  • 43
  • 77
  • 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??). – Jeff Mar 26 '11 at 17:52
  • 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. – Rob Kennedy Mar 26 '11 at 19:01
  • @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. – Disillusioned Mar 26 '11 at 23:12
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;
Cosmin Prund
  • 25,498
  • 2
  • 60
  • 104
  • Just as with the others, only the IDE shows the error, the application does not raise the exception :( – Jeff Mar 26 '11 at 16:47
  • @David - works absolutely perfect, it stops execution and everything! – Jeff Mar 26 '11 at 17:01
  • @David - that is, if I do it outside a thread, so no synchronize or anything TThread related – Jeff Mar 26 '11 at 17:02
  • @jeff kind of hard to tell. Must be something we can't see. – David Heffernan Mar 26 '11 at 17:04
  • @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. – Jeff Mar 26 '11 at 17:08
  • 1
    @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. – Cosmin Prund Mar 26 '11 at 19:43
  • @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) – Jeff Mar 26 '11 at 20:27
  • @Cosmin - it appears your solution worked afterall. :) Thanks! – Jeff Mar 26 '11 at 22:22
  • 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. – Rafael Colucci Mar 27 '11 at 14:01
  • @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. – Cosmin Prund Mar 28 '11 at 06:59
  • @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. – Rafael Colucci Mar 28 '11 at 12:19
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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • @David - what should I say then? I post the info I have – Jeff Mar 26 '11 at 15:58
  • @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... – Marjan Venema Mar 26 '11 at 16:08
  • @Jeff: It's a lot to read, but will help you enormously in asking questions the smart way: http://www.catb.org/~esr/faqs/smart-questions.html – Marjan Venema Mar 26 '11 at 16:12
  • @Marjan - I believe I did post the steps I took, and the results I got. But thanks, will keep that in mind. – Jeff Mar 26 '11 at 16:18
  • 1
    David, congrats on the gold Delphi badge. And +1 for `AcquireExceptionObject`, because I learned something new! – Cosmin Prund Mar 26 '11 at 16:34
  • @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. – David Heffernan Mar 26 '11 at 16:40
  • @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? – Jeff Mar 26 '11 at 16:53
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;
AlexV
  • 22,658
  • 18
  • 85
  • 122
Rafael Colucci
  • 6,018
  • 4
  • 52
  • 121
  • I tried that one - it's basically the same code as in the SO question I linked to. – Jeff Mar 26 '11 at 14:52
  • @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. – Jeff Mar 26 '11 at 14:57
  • well .. then i really dont know because i tried this code and it works fine here. – Rafael Colucci Mar 26 '11 at 15:05
  • 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) – Jeff Mar 26 '11 at 15:09
  • @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 :S – Jeff Mar 26 '11 at 15:26
  • 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. – Rafael Colucci Mar 26 '11 at 15:40
  • Perhaps you should close this question and create another one asking "how to stop a program execution when a thread raise a exception". – Rafael Colucci Mar 26 '11 at 15:44
  • "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)` – Rafael Colucci Mar 26 '11 at 15:46
  • 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. – Rafael Colucci Mar 26 '11 at 15:47
  • @Rafael - If I re-raise the exception in the DoHandleException, it raises nothing outside the IDE – Jeff Mar 26 '11 at 15:55
  • 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. – Rafael Colucci Mar 27 '11 at 13:53
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;
James
  • 9,774
  • 5
  • 34
  • 58
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.

Misha
  • 1,816
  • 1
  • 13
  • 16
  • Who says that it's unrelated to what the user is doing? Not everything that lives in a thread is so. – David Heffernan Mar 26 '11 at 23:49
  • 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? – Misha Mar 27 '11 at 00:22
  • @Misha 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. – David Heffernan Mar 27 '11 at 00:25
  • 1
    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. – Rafael Colucci Mar 27 '11 at 13:52
  • 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. – Disillusioned Apr 02 '11 at 08:18