Why: ConsoleApplication.UnhandledException

| | Comments (9)

One question that comes up with relative frequency is: why does ConsoleApplication.UnhandledException behave differently from Application.UnhandledException? Obviously, they both tell you when an exception hasn't been handled anywhere in the call chain. But they differ in that one has a return value, and the other does not. Why is that?

In the case of a GUI application, the event has a boolean return value. If you return true, then the unhandled event doesn't terminate the application. Instead, control is returned to the main event loop and the application can continue to run. In the case of a console application, the UnhandledException event has no return value. Most people assume that this is a bug or an oversight -- but there is a valid reason for this, and it is definitely not a bug.

There are two key concepts to understand: how do exceptions work when they're fired, and what's different about GUI and console applications.

The first key to understand is how exceptions work when they're fired. When an exception is raised, each entry in the call stack is allowed the chance to handle the exception (basically -- though this isn't 100% accurate). If it doesn't handle the exception, we unwind back another level in the call stack and allow it the chance to handle the exception. The last block on the stack is the one that's responsible for calling the UnhandledException event, and that's within the framework itself, in the main event loop.

The other key to understanding the difference boils down to the fundamental differences between a console and a GUI application. A GUI application has a main event loop, and a console application does not. So, you can imagine the underpinnings of a GUI application as looking like this:


Sub ProgramStart()
InitializeStuff
while not Done
try
RunEventLoop
catch err as RuntimeException
if not CallUserUnhandledExceptionEvent( err ) then Done = true
end try
wend
CleanUpStuff
End Sub

So when an exception happens somewhere, it happens within the event loop. If you return true from the unhandled exception event, we have a way to recover -- we simply let the main event loop continue to run. If the user returns false, then we can just terminate the application.

However, a console application's underpinnings are slightly different. Console applications have no event loop -- they just have a single event that serves as the user's application entrypoint. You can imagine that this looks something like this under the hood:


Sub ProgramStart( args() as String )
InitializeStuff
try
CallUserRunEvent( args )
catch err as RuntimeException
CallUserUnhandledExceptionEvent( err )
end try
CleanUpStuff
End Sub

In this case, if an exception happens, there is nothing to return control to! The stack has already unwound completely, and there's no possible way to recover sensibly. We can't call the Run event again, because that would be entirely unexpected and incorrect. And since we've already unwound the stack as part of the usual exception processing, there's no way to "restart" the execution at the point the exception happened (not that it would be a sensible thing to do anyways, since the code wasn't able to recover from the exception in the first place!). So we don't even allow it as an option -- if an exception isn't handled in a console application, your application will quit. The only reason for providing the event is to allow you to quit more gracefully.

9 Comments

So is putting an Exception block at the end of the App.Run event essentially identical to using the App.UnhandledException event?

What about multithreaded console apps?

@Adam -- yes, it's basically the same thing (except if you re-raise the exception, then obviously it will unwind up the call stack to the unhandled exception event anyhow). Multiple threads makes it a bit more interesting, but it's still much of the same. When you have a thread that raises an exception, it has its own call chain up until the Run method -- that's the end of the line for that entire call stack. So if the exception reaches the base of the thread's call stack, then it will fire the unhandled exception event.

So if I do the same in thread.run (put in an exception handler and resume if possible) RB requires me to re-raise ThreadEndExceptions and EndExceptions in both thread.run and app.run?
Are there any other gotchas outside the logic of my code?

@Steve -- well, for starters, you should never be doing a generic catch-all exception handler. The entire point to an exception handler is to *handle* an exception, and it's impossible for you to do so in a generic way. But yes, if you do catch either an EndException or ThreadEndException, you should always reraise it.

You compiler architects are all the same ;) I'm just trying to explore my options for a semi-generic unhandledException handler with some thread-specific options. I'll probably never write it that way but like Murphy, if it can be written it probably will eventually.
Hope your home life is idyllic.

@Steve -- lol, I know, I know -- we're all pedantic. :-P But to accomplish what you're going for: yes, those two exceptions are the only two you currently have to worry about. I can't imagine there will be others, but who knows, that's changed in the past. ;-)

Home life is awesome -- it's fun being married, though I still giggle whenever I hear "Mrs Ballman", since that's my mom's name. :-P

Aaron, I strongly disagree with your statement that one "should never be doing a generic catch-all exception handler". If the IDE follows that concept on purpose, then it's no wonder I have a reason to be angry at RS for losing my source code when the IDE crashes unexpectedly.

For example, I had it happen more than once that the editor or compiler crashes (thru a controlled RB exception), forcing me to quit the IDE (oh yes, if you just "compile", not "run", your prj is not saved!). Interim work is lost at that point. I lost many hours by that in the past years, and usually understanding where and why it happened, I cannot understand how the IDE is not able to save my project at that point.

IMO, any sensible programmer has a catch-all handler in order to give the user a last resort, e.g. enable him to save his data, or auto-save it for him.

Please keep in mind that pedantism is not helping your users, only defensive programming does.

@Thomas -- you misunderstand what I mean by catch-all exception handler. Certainly make use of the App.UnhandledException event, especially as a way to recover data if at all possible. However, I still claim that no sensible programmer should be using catch-all exception handlers like this:

try
DoSomeCode
catch err as RuntimeException
end try

This code serves absolutely no purpose since you cannot possibly *recover* from an exception like that -- you have no idea what exception DoSomeCode could throw! If you're using this sort of catch block (or any of its logical equivalents), then you should remove them and just use App.UnhandledException. It makes your code and your intentions much more clear.

My rule of thumb is: if you can recover sensibly, use try/catch blocks. If you can't possibly recover sensibly, just let it pass up the call chain.

Aaron, ah, of course. Yes, I misunderstood you, and we're actually on the same track :)

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.