Sunday, June 13, 2004

WinForms Catch-All Exception Handling

Update: Fixed the broken image link. No content changes.


When I'm writing console applications, I tend to put a try-catch block around everything in the Main loop, primarily to let me return a non-zero exit code from the application, which makes it play well with batch scripts. But it also makes it a good place to print error messages that pertain to well-known exception types. So I wind up with something like this:


static int Main(string[] args) {
  try {
    // Do application logic here
 
}
  catch (ParseCommandLineException pce) {
    Console.WriteLine("Usage error");
    return 27;
  }
  catch (Exception e) {
    Console.WriteLine(e);
    return 42;
 
}
}


When writing WinForms applications, returning a non-zero return code generally isn't necessary, since it's atypical to want to automate a GUI application using batch scripting methods. However, having a global exception filter is still a useful thing. For example, when FlexWikiPad encounters an unhandled exception, it captures the exception and displays a dialog box that gives the user the option of emailing the error message to me. This sort of feedback lets me know what error conditions users are running into most frequently, letting me fix the big PITA bugs first.


But how do you do it? Because Windows Forms applications are event-driven, there's not really a convenient place to put a top-level try-catch block. Indeed, if you try to do something like this in a simple WinForms application:


[STAThread] static void Main() {
  try {
    Application.Run(new Form1());
  }
  catch (Exception e) {
    MessageBox.Show("Error " + e.Message);
  }
}


private void button1_Click(object sender, System.EventArgs e) {
  throw new Exception("Whoops");
}


What you'll find is that when you run it, you get the following (probably familiar) window:



Note that you'll only see this error message if you run the app outside the debugger. Why? Because WinForms is making use of its own unhandled exception mechanism, and the default behavior differs based on whether you're debugging or not. This is slightly confusing, but worse it means that in some scenarios, your carefully-crafted catch block will never run.


Fortunately, you can override the default behavior, by hooking the Application object's ThreadException event, as in the following code:


[STAThread] static void Main() {
  Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
  Application.Run(new Form1());
}


private void button1_Click(object sender, System.EventArgs e) {
  throw new Exception("Whoops");
}


private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) {
 
MessageBox.Show(e.Exception.Message);
}


Now when you run the application - inside or outside the debugger - your ThreadException handler will get called whenever an unhandled exception is thrown. You can do whatever you want here - exit the application gracefully, insult the user, or use a web service to log the bug in some sort of central database.


 

32 comments:

  1. Hey Craig

    Because you mentioned making a web service call from a GUI application, here is a question for you. If you are using a typical MVC pattern to partition your GUI application and suppose one particular control in the form needs to get its initial value by contacting a webservice, would you make such a call from the View layer?

    ReplyDelete
  2. In the one GUI program I'm writing (FlexWikiPad), the View doesn't do anything but communicate events to the Controller. So to answer your question, I wouldn't do it in the view - I'd do it in an initialization routine in the controller, which would call a method on the view interface to set the initial value.

    This approach has the additional benefit of being easier to test, making TDD easier.

    ReplyDelete
  3. As sort of an author-maintained manual trackback, John Sands has good followup on this post that you should read:

    http://sandspace.com/blog/posts/261.aspx

    ReplyDelete
  4. I just have to point this out, Craig. I large number of the things you learn about WinForms and post on this blog are covered in some details in Windows Forms Programming in C# [1]. I hate to commit sacrilege here, by suggesting that you read it, however. : )

    [1] http://www.amazon.com/exec/obidos/tg/detail/-/0321116208/chrissells

    ReplyDelete
  5. I'm sure that pretty much everything I find out, someone else already knows. If I let the fact that a piece of information already exists elsewhere stop me from posting it, I'd never write anything. I merely post in the hopes that I'm telling someone something they *don't* already know.

    That said, no, I haven't read your book, and yes, I think I should. :)

    But even assuming I had read it, the fact remains that your book is in inconvenient, paper-only form that Google can't find. Oh, and I assume you'd like me (and others) to pay for it. On top of that, it's probably 300+ pages, and there are perhaps a dozen gems like the above that I actually care about.

    So I plan to keep blogging WinForms bits as I figure them out. :)

    ReplyDelete
  6. Of course there must be some renumeration for all the hard work Chris put into coming out with with the Winforms book but the simple fact is I found this post by Craig _immediately_ useful. We have a huge Winforms based desktop application that does all kinds of madness and to this date we have survived with just wrapping main() with a ordinary try/catch Exception block. Now we know what a stupid thing that has been all these days...

    ReplyDelete
  7. Of course, knowing Chris, I'm sure his comment was meant purely as "Hey, if you liked that, you'll like my book, too" which is almost certainly true. That should have been obvious to me, given that *he* clearly reads my blog. ;)

    I think my response didn't have enough smileys to indicate I understood that. :)

    ReplyDelete
  8. Of course, in support of Craig's initial response to Chris's comment... I own the book, have read it cover to cover, but it wasn't until a couple months after having read the book, and then reading Craig's comment that "two and two got put together in my head" and it all made perfect sense. Sometimes it just takes that one additional push at just the right time to make something sink in. Anyway, great work Chris, and thanks for posting this Craig!

    ReplyDelete
  9. Thanks! I think I can speak for Chris too when I say that comments like that are what keep us putting stuff out.

    ReplyDelete
  10. I'm going to point out one more thing that I just spent the past couple of hours tracking down (for the sake of the fact that Google searches turned up nothing that explicitly stated how this all works). And for Chris's sake, no I haven't checked the book yet to see if it's explicitly stated in there, but...



    The above "catch-all" only works at "catching" exceptions (meaning in the try/catch sense of the word) that occur "inside of" the Application.Run() scope.



    What I mean is, if for better or worse, you go and write some code that runs in the main() function above, after the Application.ThreadException event handler creation, but before the Application.Run() line, the ThreadException handler will receive an event for an exception that occurs in that "middle code", but will not "catch" it in the sense of the try/catch world. As a result, your application will still crash (or offer you to attach a debugger to it). The ThreadException handler WILL get a chance to "inspect" the exception, log it, etc before the application exits, but it will NOT catch it.



    So, the ThreadException handler will only "catch" exceptions that occur inside of the Application.Run() scope.



    With that in mind, the amount of code that runs "outside of" the Application.Run() scope should be kept to a minimum, at least if you want to effectively use the above "catch-all" solution.

    ReplyDelete
  11. Good point. Of course, for those errors that occur out side of Application.Run, you wouldn't be able to display any UI (since you're not processing Windows message), and since Application.ThreadException is sort of UI-related, maybe that's a good thing.



    To catch these sorts of errors hooking AppDomain.UnhandledException would probably work (I haven't tried it).

    ReplyDelete
  12. That would be a no go on the AppDomain one, unless I'm doing something wrong there too. In the R&D that I was doing on this, I was actually implementing both of these at the same time, and until I put the "errors" inside of the Application.Run() scope, the app always bombed. Any bright ideas are more than welcomed. Hopefully I'll remember to double-check Chris's book once I get home from work tonight and see if he's already got the answer in there...

    ReplyDelete
  13. What about just putting a regular ol' try/catch block in Main to handle anything that doesn't get caught by WinForms? Since it's a single method, that should be clean enough, and I'd be *stunned* if it didn't work.

    ReplyDelete
  14. Well, the thing that occurs to me as obvious (now) is that you can just put a big try/catch block around everything in Main to catch any errors that occur outside the scope of Application.Run.

    ReplyDelete
  15. Agreed on the try/catch in main(). I've already got one there to catch SecurityException (primarily for catching people running the apps off of a network share) and ConfigurationException (primarily for catching bad edits to app.config entries that someone might have made). My question, and one that I'll possibly be looking into further revolves around try/catch overhead.



    Everyone claims that there is an execution performance overhead with putting try/catch blocks around "everything" (meaning in EVERY method). Based on that, is there a similar overhead with having that "final catch-all" try/catch(Exception) block in main()? And what about just having a try/catch block in main() with just the few specific exceptions that I've listed above? For as much as I've read people's complaints about the execution performance overhead of overzealous try/catch usage, I've never seen it benchmarked anywhere in a way that I would have a definitive answer to it.



    I will agree that it's more than obvious when it comes to relying on exceptions for application logic flow, as you can literally "feel" the application slow down as exceptions get created, thrown, caught, etc.



    I will also agree whole-heartedly that overzealous use of try/catch blocks makes your code border on painfull to read.



    At the end of the day, in the fast-paced world of consulting that I work in, appearance is everything. I'd rather have a client come to me saying, hey, this doesn't seem to work properly in such and such of a situation, than have them complaining about an application that throws error dialogs in their face on a regular basis. .NET has made it extremely easy to setup a code framework for doing this in "a much better way than in the past". I'd just like to have real numbers that indicate things like execution performance issues with various methods of accomplishing this goal, and I've yet to really find them.

    ReplyDelete
  16. As with any performance question, the answer is "it depends", which is why you won't generally find many hard numbers out there...because they don't mean crap out of context. The answer is to try it and measure. Fortunately, this is easy to do.



    That said, my uniformed opinion is that that "everyone" is smoking crack if they think that the mere presence of even many catch statements is going to have a noticable impact on perf in a WinForms application. I'd be surprised if the try block setup is very expensive at all.



    On the other hand, *catching* exceptions is definitely expensive. It's expensive enough to be able to perceive it in some WinForms apps. But if your app never actually throws any exceptions, it shouldn't matter. And when it does, you probably have other things to worry about than perf. And if you're throwing them often enough to have to worry about perf, then you *definitely* have other problems. :)

    ReplyDelete
  17. Agreed on the performance stuff :P



    In an effort to hopefully bring this all to an end, seeing how I hate starting something and not ending it, it turns out that Chris's book was good in pointing out what you originally did above (as Chris pointed out to you). But it wasn't until I dug into Jeffrey Richter's "Applied Microsoft .NET Framework Programming" book that I found the definitive answers to all of this "unhandled exception stuff". It's ritten right there on pages 438-441 in very simple English. As I originally stated about the stuff in Chris's book, sometimes it takes the right situation and reading it for the 3rd or 4th time for it to finally make sense...

    ReplyDelete
  18. Great! So what *is* the definitive answer?

    ReplyDelete
  19. Cool. Thanks for the link.

    ReplyDelete
  20. Je suis desole mais je parle suelement un petite peu de Francais.



    Essayez ceci: http://babelfish.altavista.com/

    ReplyDelete
  21. Can I just say - in response to someone who said two years ago that this was already in the "Windows Forms Programming in C#":



    I've read the book. I've also read the second edition - but I've spent the last day and a half trying to figure out why when I debug code, my catch blocks are used & when I don't debug, they're not...



    Thanks Craig for ending my search.



    (And yes the answer could be in the books - but I sure didn't see it.)

    ReplyDelete
  22. Having read this thread, I'm appalled at the comments by Chris Sells in particular. If it wasn't for people like Craig we wouldn't get good answers fast.



    I suggest that if Chris already know's it all, then he should start his own blog.

    ReplyDelete
  23. Chris is a good friend. As I explained above, I took his comments to mean "If you liked that, you'll like my book." Which I'm sure is true.



    Also, Chris has a pretty good blog already. I highly recommend it: http://sellsbrothers.com.

    ReplyDelete
  24. Thanks, your entry has helped me very much!

    ReplyDelete
  25. Thx, searched a long time for this answer!

    ReplyDelete
  26. great article...thanks!!!!

    ReplyDelete
  27. Finally found the right combination of search terms in Google that brought me to the answer: this page.

    THANK you.

    ReplyDelete
  28. Thanks for posting this. Even 4 and a half years later it was helpful to me as I was searching for an answer to this question.

    ReplyDelete
  29. Hi,
    thanks for this great tip!
    5 years later and it still is an amazing way of catching all exceptions.

    but, it seems to work only in the main thread. It doesn't seem
    to catch exceptions from other threads that run simultaneously.

    is there a way to catch those too?

    thank you in advance

    ReplyDelete
  30. Have you tried AppDomain.UnhandledException?

    ReplyDelete
  31. Ohhhhhhhhhhhh man I always thought you needed a try/catch block for every Exception I want to handle. Why didn't I look for this a long time ago?!

    THANKS!

    ReplyDelete
  32. and here is an article from 2004 with the same info + more insights:
    http://msdn.microsoft.com/en-us/magazine/cc188720.aspx

    ReplyDelete