February 21, 2012
On 2/20/12 7:22 PM, H. S. Teoh wrote:
> Please bear in mind, I'm not saying that Variant[string] is *completely*
> useless. I'm just saying that most of the time it's not necessary. Sure
> there are some cases where it's useful, I've no problem with it being
> used in those cases. But we shouldn't be using it for all kinds of stuff
> that can be handled in better ways, e.g., static fields in a derived
> exception class.

On the contrary, I predict that once Variant[string] info() is present in Exception, people will start using it with a collective sigh of relief.

Andrei

February 21, 2012
On 2/20/12 7:32 PM, Juan Manuel Cabo wrote:
> Well... then why did this mistakes exist?:
>
> In dot NET:
>
> 	ComException - Exception encapsulating COM HRESULT information
>          SEHException	Exception encapsulating Win32 structured exception handling information.z
>
> 	http://msdn.microsoft.com/en-us/library/z4c5tckx%28v=VS.71%29.aspx
>
> And why do you think that a thing like standardizing DatabaseException
> never survives users, and that each database manager library defines its
> own top *DatabaseException base class?
>
> This is a universal problem with transversal traits of exceptions.

Yes, exactly. Exceptions are poorly equipped to address cross-cutting concerns, which is odd because they are themselves a cross-cutting matter :o). The solution is, I think, to find improved abstractions.


Andrei
February 21, 2012
On 2/20/12 8:33 PM, Robert Jacques wrote:
> Variant e = new MyException();
> writeln( e.filename, e.line, e.column);
>
> Aren't __traits and opDispatch fun?

I thought the same, but was afraid to bring the nuclear bomb out :o).

Andrei
February 21, 2012
On Mon, Feb 20, 2012 at 08:32:08PM -0600, Andrei Alexandrescu wrote:
> On 2/20/12 7:08 PM, H. S. Teoh wrote:
> >On Mon, Feb 20, 2012 at 06:38:08PM -0600, Andrei Alexandrescu wrote: [...]
> >>>We shouldn't be using Variant[string] for this, because there's another problem associated with it: suppose MyException sometimes gets "extra_info1" and "extra_info2" tacked onto it, on its way up the call stack, and sometimes not. Now what should the catcher do?
> >>
> >>Use a central loop to render the information.
> >
> >Then it's useless for i18n, unless you can magically translate a sequence of unknown values into a different language in a consistent way.
> >
> >It sounds like all this is good for is to print a list of "name=value" pairs. Which is useful, I'll admit, for development/debugging purposes. I don't see how such a thing is useful in i18n.
> 
> As I said, string templates in conjunction with name/value bindings is all that's needed for i18n.

And how would such templates be designed, if what is available in the bindings changes depending on the code path that led to the exception?

I agree with you that this allows the catch block to do stuff like pass the entire hash to the i18n formatter without needing to know what's in it. And conceivably, the formatter doesn't need to know either, it just gives the bindings to the string templates. But the string templates themselves have to know what's in the bindings. In fact, they *assume* that certain bindings are in there. And when the expected bindings are not there, then the template can't be applied. And you couldn't possibly know this until runtime.

Whereas if the fields were fixed at compile-time, then the compiler could verify that what the code thinks is there, is actually there.


[...]
> Exactly, and with this you just destroyed your own design. For it does not build any polymorphic interface, no abstraction. It forever forces code to toil in the concrete (_fileName, _ipAddress, _userName) and fails to elevate any common interface that would foster reusable code.

No. With RTTI, there's no need to toil in the concrete at all. Loop over whatever fields happen to be in the object, hand them off to the formatter or whatever it is wants to use them, job finished.

The only time you explicitly refer to _fileName, _ipAddress, etc., is when you *specifically want to deal with them*. This is where you *want* the compiler to statically verify that _fileName actually exists in that object. Rather than wait till runtime and then the catch block realizes oops, the field doesn't exist.

I'm not negating the fact that the hash is useful for *some* things. I'm just saying that there are occasions where it *shouldn't* be used. There are times when you need compile-type type-checking.


[...]
> On the contrary. This builds abstraction:
> 
> class Exception
> {
>     bool transient();
>     Variant[string] info();
>     ...
> }
> 
> because it allows client code to treat different types, even types that haven't even been defined yet, uniformly.

I see your point. But doesn't RTTI already fill the need of genericity? Why not take advantage of compiler-time type checking where it's possible?

You can still have the hash for runtime-added stuff, like adding properties to exceptions in transit. I can see a use for that. But why must *everything* be stuffed into the hash?


[...]
> >Then pray tell how "simple iteration" will achieve the translation of the data in Variant[string] into a localized string, when there is no guarantee any field will be in the hash at all. Format strings obviously won't work, since you can't have a format string unless you already know what arguments are present.
> 
> There's no guarantee. That'll be a runtime check in the formatting engine.
[...]

So in order for the format string to *not* fail at runtime, the
exception must always have the same fields in the bindings, right? Isn't
that the same thing as defining the fields statically and using RTTI
to iterate over them? That way, the compiler can verify at compile time
that the fields are actually there.


T

-- 
Real Programmers use "cat > a.out".
February 21, 2012
On Mon, Feb 20, 2012 at 08:41:47PM -0600, Andrei Alexandrescu wrote:
> On 2/20/12 8:33 PM, Robert Jacques wrote:
> >Variant e = new MyException();
> >writeln( e.filename, e.line, e.column);
> >
> >Aren't __traits and opDispatch fun?
> 
> I thought the same, but was afraid to bring the nuclear bomb out :o).
[...]

Mmm... but *this* nuclear bomb, I like. :-)


T

-- 
Heads I win, tails you lose.
February 21, 2012
On Monday, February 20, 2012 20:36:16 Andrei Alexandrescu wrote:
> On 2/20/12 7:07 PM, Jonathan M Davis wrote:
> (Cloning and comparison are also essential polymorphic methods to
> implement.) But then again the view "exceptions are wildebeests" is
> coming straight from inside the box. We want to improve on that.

Neither of which need to be done much with exceptions.

> I don't think inheritance without subtyping would help any. C++ has it (non-public inheritance) but that didn't improve exceptions one bit.

I don't think that it would necessarily improve them. My point was that polymorphism isn't of much use for exceptions. So whether they are polymorphic or not is generally irrelevant.

> > It doesn't really hurt us that exceptions have polymorphism, but it
> > doesn't
> > generally help them do their job at all. If you're going to do anything
> > beyond simply print an error message, you need to know what the exception
> > type is and what data it has. And that's not OO at all.
> 
> Or you need to implement a key-value interface that allows printing of an elaborate error message.

You need to know the type regardless. And the information that an exception is going to have is very strongly tied to its type. I'd argue that in the general case, using a key-value interface is a huge step down from having the information be fields in the type that it goes with. It may help with printing elaborate error messages, and it would allow you to add information to an exception which wouldn't generally be there, but it helps little-to-none in actually handling exceptions and in fact makes it worse, because then you're moving what would be compilation errors to runtime.

It may be that the hashmap may be worth adding, but I'd consider it bad practice to use it when putting the variables in the type itself would work. It's only when you need to add information which wouldn't normally be in the exception that it makes much sense.

- Jonathan M Davis
February 21, 2012
Am 20.02.2012 21:49, schrieb Andrei Alexandrescu:
> On 2/20/12 1:32 PM, Juan Manuel Cabo wrote:
>>  So, if your boss wants the URL of the request that was made
>>  when the standard library threw you a FileNotFoundException,
>>  you can do:
>>
>>
>>  	try {
>>  	...
>>           } catch (Exception ex) {
>>                   //Rethrow the exception with the added detail:
>>  		ex.details["custom_url"] = getenv("URI");
>>                   throw ex;
>>           }
>
> That's a very interesting angle!
>
> Andrei
>
>

that is sooooo bad - fuck signatures just add information ...

now im able to just catch always Exception (sweet) ... but my damn handle code needs to know the real interface ... crippled into details["custom_url"]... so in the end we will come to if( detail[x] is in there) if( detail[y] is in there ) if ( detail[z] is in threre ) shit

i agree 100% - its easy for just giving out the information, but awfull for handling the exception
February 21, 2012
On 2012-02-20 21:25, Nick Sabalausky wrote:
> "H. S. Teoh"<hsteoh@quickfur.ath.cx>  wrote in message
> news:mailman.704.1329767254.20196.digitalmars-d@puremagic.com...
>> On Mon, Feb 20, 2012 at 08:36:56PM +0100, Andrej Mitrovic wrote:
>>> On 2/20/12, Juan Manuel Cabo<juanmanuel.cabo@gmail.com>  wrote:
>>>> will be trouble. Instead please do:
>>>>
>>>>          "The '%1$s' file's size is %2$d which is wrong"
>>>>
>>>
>>> That is the shittiest formatting specifier ever invented. The
>>> unreadability of it is why I never, ever, use it. Python solved this
>>> nicely with its {0} {1} syntax:
>>>
>>>>>> print '{0} and {1}'.format('foo', 'bar')
>>
>> Actually, even that isn't ideal. How is the translator to know what on
>> earth {0} and {1} are? Sometimes you need to know in order to make a
>> good translation. This would be even better:
>>
>> "The ${file}'s size is ${size}, which is wrong"
>>
>> The usefulness of named arguments is even more apparent in complex
>> message like this one:
>>
>> "${file}:${line}: Expecting ${expectedtoken}, got ${inputtoken}"
>>
>> Without named parameters, you'd have:
>>
>> "{0}:{1}: expecting {2}, got {3}"
>>
>> which is almost impossible to translate. What are {0} and {1}? What are
>> {2} and {3}? Does it mean "12:30pm: expecting program to succeed, got
>> general protection fault"?
>>
>> Using named parameters makes it clear this is a parser error, not
>> something else. This difference may mean using a completely different
>> grammatical structure to translate the message.
>>
>
> vote++; I've been drooling over the idea of named argument format strings in
> D for a long while.
>
> I also agree with posix-style specifiers being barely readable. I fell in
> love with C#'s "Hello {1}" style at first sight, and was thrilled that Tango
> adopted it. Then I moved to Phobos2, and have been missing those wonderful
> curly braces ever since.

Tango doesn't even require you put a number in the parameter, just "Hello {}". It is still possible to put a number if you want, something like this "Hello {2} {1} {1}".

-- 
/Jacob Carlborg
February 21, 2012
All of this heated debate has led me to reconsider our whole concept of exceptions. It seems that we're squabbling over little details in existing paradigms. But what of the big picture? What *is* an exception anyway? We all know the textbook definition, but clearly something is missing since we can't seem to agree how it should be implemented.


DEFINITION

So I'd like to propose this definition: an exception is an abnormal condition that causes a particular operation not to be completed, *but which may have one or more ways of recovery*.

I'm not interested in problems with no method of recovery: we might as well just terminate the program right there and call it a day. The fact that there's such a thing as try-catch means that we're interested in problems that we have *some* way of handling.

Before I proceed, I'd like to propose that we temporarily forget about the current implementation details of exceptions. Let's for the time being forget about class hierarchies, try-catch, Variant hashes, etc., and let's consider the *concept* of an exception.

In discussing what is an exception, we can jump into the nitty-gritty details, and argue all day about how to handle the particulars of case X and how to reconcile it with case Y, but I'd like to approach this from a different angle.  Yes, if we know the nitty-gritty, then we can deal with it in a nitty-gritty way.  Let it suffice to say that sometimes, we *want* to get into the nitty-gritty of an exception, so in any implementation there should be a way to do this.

But let's talk about the generics.  What if we *don't* know the nitty-gritty? Can we still say something useful about exception handling *independently* of the details of the exception? If module X calls module Y and a problem happens, but X doesn't understand the implementation details of Y or how the problem relates to it, can X still do something meaningful to handle the problem?

To this end, I decided to categorize exceptions according to their general characteristics, rather than their particulars. In making this categorization, I'm not looking for artificial, ivory-tower idealizations of exception types; I'm looking for categories that would allow us to handle an unknown exception meaningfully. In other words, categories for which *there are recovery methods are available* to handle that exception. I don't care whether it's an I/O error, a network error, or an out-of-memory error; what I want to know is, *what can I do about it*?


TRANSITIVITY

One more point before I get into the categories themselves: in finding useful categorizations of exceptions, it's useful to find categories which are *transitive*. And by that I mean, a category C is transitive if an exception E in this category remains in the same category when the stack unwinds from Y, where E was first thrown, to X, which called Y. For example, Andrei's is_transient property constitutes a transitive category. If X calls Y and Y experiences a transient error, then by extension X has also experienced a transient error. Therefore, the is_transient category is transitive.

Why should we care if an exception category is transitive? Because it allows us to make meaningful decisions far up the call stack from where the actual problem happened. If an exception category is not transitive, then as soon as the stack unwinds past the caller of the function which threw the exception, we can no longer reasonably assume that the exception still belongs to the same category as before. An illegal input error, for example, is not necessarily transitive: if X calls Y and Y calls Z, and Z says "illegal input!", then it means Y passed bad data to Z, but it doesn't necessarily mean that X passed bad data to Y. It may be Y's fault that the input to Z was bad. So trying to recover from the bad data when the stack has unwound to X is not useful: X may not have any way of changing what Y passes to Z. But if Y merely takes the data passed to it by X and hands it to Z, then the illegal input *is* transitive up to X. In *that* case, X can meaningfully attempt a recovery by fixing the bad data. But if X was the one who created that data, then X's caller cannot do anything about it. So input errors are only conditionally transitive -- up to the origin of the input data.


CATEGORIES

Here are the categories I've found so far. I don't claim this is anywhere near complete, but I'd like to put it on the table so that y'all can discuss about it, and hopefully refine this idea better. Each category is associated with a list of recovery actions that are meaningful for that category. Note that it doesn't mean that *every* exception in that category will have all listed recovery actions available; some recovery actions aren't possible in all cases. In an implementation, there would need to be a way to indicate which of the listed recovery actions are actually possible given a particular exception.

Also, the recovery actions are deliberately generic. This will be explained later, but the idea is to let generic code decide on a general course of action without knowing the details of the actual implementation, which is determined by the code that triggered the original condition, or by an intermediate handler midway up the call stack who knows how to deal with the condition.


INPUT ERRORS:

Definition: errors that are caused by passing bad input to function X, such that X doesn't know how to compute a meaningful output or execute a meaningful operation.

Transitivity: conditional, only up to origin of the input data.

Recovery actions:
- Attempt to repair the input and try the operation again. Only possible
  if there exists a mechanism of repairing the input.

- Skip over the bad input and attempt to continue. Only applicable if
  the input is a list, say, and the program can still function (albeit
  not to the full extent) without the erroneous input. Otherwise, this
  recovery action is meaningless.


COMPONENT FAILURE:

Definition: the operation being attempted depends on the normal functioning of sub-operations X1, X2, ..., but one (or more) of them isn't functioning normally, so the operation can't be completed.

Transitivity: Yes. If X calls Y and Y calls Z, and one of Z's subcomponents failed, then from Y's perspective, Z has also failed.

Recovery actions:
- Retry the operation using an alternative component, if one is
  available. For example, if X is a DNS resolver and Y is a
  non-responding DNS server, then X could try DNS server Z instead.
  Transitively, if X can't recover (doesn't know an alternative DNS
  server to try), then X's caller can attempt to bypass X and use W
  instead, which looks up a local DNS cache, say.  (Note that the
  high-level code that handles a component failure does not need to know
  the details of how this component swapping is implemented, or exactly
  what is being swapped in/out. It only knows that this is a possible
  course of action.)


CONSISTENCY ERROR:

Definition: the operation being attempted depends on a suboperation X, which is operating normally, however, the result returned by X fails a consistency check. (I'm not sure if this warrants separate treatment from component failure; they are very similar and the recovery actions are also very similar. Maybe they can be lumped together as a single category.)

Examples: Numerical overflow/underflow, which would throw off any remaining calculations.

Transitivity: Yes. If X calls Y and Y calls Z, and Y discovers the Z produced an inconsistent result, then by extension Y would have also produced an inconsistent result had it decided to blindly charge ahead with the operation.

Recovery actions:
- Retry the operation by using an alternative component, if available.
  For example, a numerical overflow might be repaired by switching to a
  BigNum library for the troublesome part of the computation.


LACK OF RESOURCES:

Definition: the operation being attempted would have completed normally, had there been sufficient resources available, but there aren't, so it can't continue.

Transitivity: Yes. If X calls Y and Y runs out of resources to finish, then by extension X doesn't have the resources to finish either.

Recovery actions:
- Free up some resources and try again. This one is debatable, since it
  may not be clear which resources need to be freed up, or whether they
  *can* be freed at all. If it's a full disk, for example, it would be
  unwise to just go and randomly delete files. But some cases can be
  handled, e.g., if memory runs out, trigger the GC. (But presumably the
  GC does this automatically, so this may not be an actual use case that
  needs manual handling.) All in all, this category may not be easy to
  recover from, so it may be of limited utility.


TRANSIENT ERROR:

Definition: the operation depends on component X, which is known to sometimes fail. Example: a network server may sometimes go down due to intermittent network problems, timeouts, etc..

Transitivity: Yes. If operation X calls operation Y and Y has a transient error, then X also has a transient error by extension.

Recovery actions:
- Retry the operation: it may succeed next time.

Sidenote: Here I'd like to say that at first, I was very skeptical about Andrei's is_transient proposal, because I didn't have the proper context to understand its utility. I felt that something was missing.  And that missing something was that is_transient is but a part of a larger framework of generic exception categories. Without this larger context, the value of is_transient is not immediately obvious. It seems like just an arbitrary thing out of the blue. How could it possibly be useful?? But when viewed as part of a larger system, is_transient can be seen to be an extremely useful concept: it is a *transitive* category, which means you can do something meaningful with it at any point up the call stack.


CREDENTIALS ERROR:

Definition: there's no problem with the input, and all subcomponents are functioning properly, but because of lack of (or improper) credentials, the operation could not be completed.

Transitivity: Yes(?). Not sure about this one, not because it doesn't fit the definition, but because it's unclear how to correctly handle the recovery action. A single operation may consist of many sub-operations, each requiring a different set of credentials. Just because one of the sub-operations raises a credentials error doesn't mean the exception handler knows where to find alternative credentials, or even what kind of credentials they are.

Recovery actions:
- Retry the operation with different credentials. E.g., prompt user for
  a different password. But I'm unsure if/how this can be generally
  implemented, as described above.


These are all the general categories I found. There may be more.


IMPLEMENTATION

Alright. All of this grand talk about generics and categories is all good, but how can this actually be implemented in real life?

The try-catch mechanism is not adequate to implement all the recovery actions described above. As I've said before when discussing what I called the Lispian model, some of these recovery actions need to happen *in the context where the exception was thrown*. Once the stack unwinds, it may not be possible to recover anymore, because the execution context of the original code is gone.

One peculiarity about Andrei's is_transient is that you *can* re-attempt the operation after unwinding the stack. Which is what makes it useful in the current try/catch exception system that we have.

But not all recovery actions can be implemented this way. Some, such as repair bad input, or try alternate component, makes no sense after the stack has unwound: the execution context of the failing component and its caller is long gone; to try an alternate component would require painstaking passing of retry information all the way down the function call chain, polluting normal function parameters with retry parameters and producing very ugly code. Repair bad input, in particular, *must* be done before the stack unwinds past the origin of the input, otherwise it's impossible to correct it.

This is where the Lispian model really shines. To summarize:

1) When we encounter a problem, we raise a Condition (instead of throw
an exception immediately).

2) Every Condition is associated with a set of recovery actions. These actions are generic; basically we're mapping each exception category to a Condition. The raiser of the Condition will specialize each recovery action with code specific to itself.

3) High-level code may register Handlers (in the form of a delegate) for particular Conditions.  These registrations are limited by scope; once the function registering the handler exits, any handlers it registered are removed from the system. The handler registered closest to the origin of a Condition has priority over other matching handlers.

4) When a Condition is raised, the condition-handling system first checks a list of registered condition handlers to see if any handler is willing to handle the condition. The handler is passed the Condition with its associated set of recovery options. The handler decides, based on high-level information, which recovery action to take, and informs the condition-handling system. The recovery action is then executed *in the context of the function that raised the Condition*, *without unwinding the stack*. If no handler is found, or the handler decides to abort the operation, then the condition-handling system converts the Condition into an exception and throws it. A function higher up the call chain may decide to catch this exception and raise a corresponding Condition, to allow (other) handlers to deal with the situation at the higher level. If nothing is caught or all attempts to fix the problem failed, we eventually percolate up the call stack to the top and fail the program.

Advantages of this system:

- Complex recovery actions are possible, because we don't unwind the
  stack until we decide to abort the operation after all.

- Recovery actions run in the context where failure is first seen,
  thereby taking advantage of the immediate context to recover in a
  specific way.

- High-level code gets to make decisions about which recovery action to
  pursue (via the delegate handler). It gets to do this *without* need
  to know the nitty-gritty of the low-level code; it is given the
  generic problem category and a list of generic recovery actions that
  can be attempted. The low-level code implements various recovery
  options, the high-level code chooses between them.

- If nobody knows how to handle the situation, we unwind the stack, as
  in the traditional try/catch model.

- If an intermediate function up the stack has a way to deal with the
  situation, it can catch the associated exception and raise a Condition
  that has recovery actions *run at its level*. The high-level delegate
  still gets to make decisions, but now the recovery actions are run at
  a higher level than the original locus of the problem. In some cases,
  this is a better position for attempting recovery. E.g., a network
  timeout may be seen at the packet level, but to repair the problem
  requires reconnecting from, say, the HTTP request level, so we need to
  unwind the stack up until that point. This is actually superior to the
  try/catch mechanism, because at the HTTP request level, we don't
  necessarily have enough context to decide what course of action to
  take; but by passing the condition a higher-level delegate, it can
  make decisions the HTTP module can't make, and the HTTP module can
  correct the problem without unwinding the stack all the way to where
  the delegate was registered.


In a previous post, I had a skeletal implementation of this system, but the major problem was that it was too specialized: every piece of code that wanted to implement recovery needed to define a specific Condition with its own set of recovery strategies, leading to reams and reams of code just to achieve something simple. Furthermore, the high-level handler needed to know the nitty-gritty low-level details of what each Condition represented and what options are available to deal with it, so there was no way to write a *generic* handler that can decide what to do with conditions whose details it knows nothing about.

But by using generic exception categories, we can finally get rid of that bloat and still be able to implement problem-specific recovery strategies. The high-level code need only know which generic category the Condition belongs to, and based on this it knows which recovery actions are available. It never needs to know what the details are (unless it's intended to be a very specific handler dealing with a very specific condition whose details it knows). The low-level code provides the implementation of the recovery actions by implementing the generic interface of that particular category.


Currently, I'm still unsure whether Conditions and Exceptions should be unified, or they should be kept separate; deadalnix recommended they be kept separate, but I'd like to open it for discussion.


Sorry for this super-long post, but I wanted to lay my ideas out in a coherent fashion so that we can discuss its conceptual aspects without getting lost with arguing about the details. I hope this is a step in the right direction toward a better model of exception handling.


T

-- 
Life is complex. It consists of real and imaginary parts. -- YHL
February 21, 2012
Am 20.02.2012 22:11, schrieb Juan Manuel Cabo:
> Yeah.. that is a problem! :-) Thanks for liking the idea, now we can
> talk about the fine details!!
>
> One way is to not let the user direct access to the associative array,
> but wrap the e.info["MyDetail"] call in a nothrow function, such as
> e.info("MyDetail"), and an e.hasInfo("MyDetail"), and of course:
> e.addInfo("MyDetail", value) and e.allInfoNames() or something.

if the key is missing i can't recover/control the error - now all my data is gone - a silly wraper which catches the key-does-not-exists-lets-just-return-null will not help here