Pop quiz time! What would the user see if this code is executed in your application?
Parallel.Async(
procedure
begin
raise Exception.Create('Exception in Async'); end);
The answer may surprise you: Nothing! At least if you’re not using the latest SVN version.
For a long time exceptions were not something I thought about while developing OmniThreadLibrary.
[I don’t really like to use exceptions (although I will admit that they are useful) and even when I use them I never allow them to “escape” to the global context and become “visible” at the VCL level. In other words, if my application causes a message box with an exception text to pop up, this will indicate an error in my design, not an intention.]
During the OTL history there were quite some changes on how exceptions are handled, but at the end I’ve settled down with the Delphi model – if your TThread descendant raises an exception that is not handled in your code, this exception will be silently ignored. No notification, no log, no popup – it will just disappear.
[Of course, I’m only talking about the end-user experience here. If you run the program in the RAD Studio with debugging enabled, debugger will catch the exception and display a message box.]
While this is good for low-level programming, high-level constructs (i.e. the OtlParallel unit) are not really focused on threads and tasks but on parallel execution of frequently-used parallel architectures. In such context, exceptions can sometimes be used to simplify the program flow. That’s why some of the high-level wrappers got a proper exception handling after the 2.1 release ([1], [2], [3]). Async, however was not one of those. Until few SVN revisions ago, the code above would therefore generate an exception which will later disappear.
This has changed few days ago and again yesterday when I committed few simple fixes for that behavior. Async will now install its own OnTerminated handler and re-raise task exception in it. If you try the same code with the latest revision, you’ll see a message box provided by the VCL exception handler.
What about your code – can it get access to the unhandled exception before the VCL sees it? Sure, just write your own termination handler and handle exception there. Following code explains this approach.
Parallel.Async(
procedure
begin
raise Exception.Create('Exception in Async'); end, Parallel.TaskConfig.OnTerminated(
procedure (const task: IOmniTaskControl)
var
excp: Exception;
begin if assigned(task.FatalException) then begin
excp := task.DetachException;
Log('Caught async exception %s:%s', [excp.ClassName, excp.Message]);
FreeAndNil(excp); end; end));
All this is also demoed in the updated 48_OtlParallelExceptions demo.
Sorry, but this is not a good path.
ReplyDeleteSince the library was historically catching all excepts, in silent, the new configuration should be to 'throw' them, and not the opposite. If I just update the OTL lib and not touch my code, the bahavior will change where I didn't expect. And this is bad in so meny ways.
Live with it.
DeleteException handling at the low level will stay silent. Exception handling on high level will gradually change towards the point where all high-level constructs will allow exceptions to flow through them (but they will also give you an option to handle it).
There should be no exceptions visible to the OTL layer unless you are knowingly using them to signal exceptional states and if this is the case you should also handle them in the task owner. Everything else is a bad bad bad design.
I m think that Exception handling worth sample application and separate blog post - about how to properly handle exceptions and possible exception strategy for applications!
ReplyDeleteIt seems to me, when Windows SEH and exceptions were first introduced, they were developed with no multithreading in mind. Exception is the thing that allows to control (in a quite tricky way) the execution flow of one thread. It is a bit similar to aspect-oriented programming, because after exception is raised _anywhere_, control flow immediately moves into nearest except..end (and of course finally..end) blocks. Exceptions are also logically connected with execution stack (which is also separate for each thread), because usually when exception is raised, we want to know the execution stack of the call which caused the exception to trace out the error.
ReplyDeleteIn multithreaded application everything changes greatly. We still can use exceptions in each individual thread, but only main thread unhandled exceptions will automatically show as a dialog box to user (thanks to Delphi's Application.Run implementation). In such a great product as the OTL is, Primoz allows us to write multithreaded code in a very simple, fluent and synchronous-alike manner. In this case, of course, mechanisms for automatic propagating exceptions from child threads into the main thread must be a number one feature, because no one can write exception-free code (for example, "Out of memory" exception can happen always). Hope, nobody here uses "try DoSomething; except {do nothing} end;" anti-pattern ;)
Thanks, Primoz, for your great work! If you'll be planning to write a blog post on exception handling, feel free to contact me on examples and anything exception-related, because exceptions is the thing I use all the time and it helps me really really much.
Actually I've been planning a blog post on exception handling, but only to say: "exceptions are bad, don't use them."
Delete>"exceptions are bad, don't use them."
ReplyDeleteUmmm, I'm a bit frustrated.. There's no error-free code. Ever. The only question is, do we implement error handling in our code or not, and how convenient this implementation is (for the end-user on one hand and for the programmer on the other). If the code is written for one-time use or errors are very likely not expected, I won't implement smart error hadnling. Sincerely, I'll do almost nothing about error handling and default Delphi exception implementation will to the rest in this case. But in complex commercial product everything changes. Especially, if this product has rich GUI. In case your product is a driver, controller or some kind of a non-GUI service, error handling strategy may be the other.
I don't believe, that error handling in robust modern system (which has a GUI :) ) can be implemented in old-school "error code" (or HResult) way, because in this case 1) there should be error checking code after each and every method or function call, because each method can meet some error condition 2) we still cannot totally avoid exception handling, because there are "system exceptions", for example, division by zero (and many others). Writing safe code is hard, much harder than writing the "same" unsafe code, which does the same things assuming there's no errors, special situations etc. Exceptions and structured exception handling is the simplest way to make your code safe without titanic efforts and numerous lines of (still error-prone) error-checking code. All I want to say "If you want your code to remain simple and safe at the same time, use exceptions".
Yes, I believe error codes are vastly superior to exceptions and I'm always using error codes. Maybe, in some very specific areas (for example when a parser may reach end of stream at any moment) I will use exceptions to pop up through the layers of code but that will always happen in the private code of the library/module. This exception will be caught by the upper layers and error code will be returned to the caller.
ReplyDeleteIn short - I believe that error conditions on module/library barrier must always be expressed as error codes and not as exceptions. There's one good reason for this - error codes are announced and exceptions are not. (If you look at the first kind of code, you will immediately see that an error code is returned and then you'll find out what errors can be returned. If you look at the second kind of code, you won't have any idea that an exception can be triggered inside and you won't know that you have to handle it.)
System exceptions are completely different beasts and I will always handle them in a same manner - dump stack trace for all threads in the program, report the exception and exit the program.
I think the main difference between exceptions and error codes is the fact, that error codes are too easy to ignore. You just don't check for it, and your app may go crazy. On the other hand, exceptions are hard to ignore (which is good, because we should never ignore errors if we want our code be safe). Even if you don't write any exception handling code, exception will be automatically handled by Delphi on the topmost level and the error message will be shown to user (which is also good). To ignore the exception, you'll have to write additional code (try..except {do nothing} end) which clarifies your intention to ignore errors in this particular place (which I believe is still a very bad pattern).
ReplyDeleteBoth patterns (error codes and exceptions) proved theirselves as good practices, but writing safe code using each of the ways is still quite hard and requires a lot of attention from developer.
>>dump stack trace for all threads in the program, report the exception and exit the program
Oh, how I wish it was that simple. Unfortunately, often it turns out that stack trace contain too little information to reproduce an error. Often I like to know values of parameters the fuctions in the stack were called with. Sometimes to reproduce an error user have to do very long and special chain of actions (open this window, do "this", close it, open another window, do "that" and here he faces an error condition (let it be Access Violation). And what will he tell me, if his app will just silently close at the moment? Stack trace of an AV call will not help me at all, becuase the error was caused by some prevoius action. I often ask users to show me (on their screens), what they are doing, to reproduce an error. Sometimes it takes time to identify the special chain of actions that lead to error. Closing an app is not the best way in this case. Here I'm still talking about interactive GUI apps. If your app is a service, which don't have a GUI and don't interact with the user, your strategy may suit better.
I can be happy either way, to be honest. I just want to know what the possible errors from your library are and when they are likely to arise.
ReplyDeleteNick Hodges "wide but shallow" exception pool IMO is very close to a "list of error codes". The advantage of exceptions is the detailed stack trace and somewhat tidier code. The disadvantage is that you still have to use error codes, even implicitly, because 80% or more of programming is dealing with the special cases. What counts as a "normal special case" and what's an "exceptional special case" is often a question of whether you've written the code to deal with it yet.
This post by Raymond Chen was one of the things that persuaded me that it wasn't just me that was struggling to deal with exceptions nicely:
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
I've come to the conclusion that exceptions suit trial-and-error programming more than thoughtful programming. You write some code, run it to see what happens, write code to stop getting the exceptions, run it ... then go back to writing more code. There doesn't seem to be any other way to know what can happen when you run exception-based code. And it is very frustrating when I'm trying to design something. I'd rather someone was forced to a 64 bit error code than threw apparently random exceptions from the bowels of their library. I mean, 4 billion different error codes is a bit excessive, but if that's not enough, use a 64 bit value by all means.
Error codes also tie into contractual programming quite well. Getters don't have side effects, so "GetFoo" can't throw an exception or return an error, full stop. So it can return Foo. But "UpdateFoo" doesn't have a return value, so it can throw/return an error, and it's purely coding convention that says "function GetFoo" returns a value, "UpdateFoo" returns an error code. Except in strongly typed languages, where TErrorCode = integer gives you something that's not assignment compatible with an integer. Hence in Delphi using enums a lot... TUpdateFooErrors = (fooInvalidValue...); You could think of them as very, very specific exceptions if you like.