The Right Approach to Exceptions

H. S. Teoh hsteoh at quickfur.ath.cx
Mon Feb 20 14:04:20 PST 2012


On Mon, Feb 20, 2012 at 03:12:08PM -0600, Andrei Alexandrescu wrote:
> On 2/20/12 2:53 PM, H. S. Teoh wrote:
> >On Mon, Feb 20, 2012 at 05:31:28PM -0300, Juan Manuel Cabo wrote:
> >>>...
> >>>Sure. Again, this is not advocating replacement of exception hierarchies with tables!
> >>>...
> >>>
> >>>Andrei
> >>>
> >>
> >>I think that the case of rethrowing an exception with added detail is
> >>the worst enemy of clean Exception hierarchies.
> >
> >Hmm. This is a valid point. Sometimes you want to add contextual details
> >to an exception in order to provide the final catching code with more
> >useful information. Otherwise you may end up with a chain of mostly
> >redundant exception classes:
> [snip]
> 
> Yah, it does look we need to better investigate the Variant[string]
> info() primitive.
[...]

I still don't like the idea of using Variant[string], though.

(1) It doesn't allow compile-time type checking. This is a big minus, in
my book.

(2) It's overly flexible. Anyone along the call stack can insert
(hopefully NOT delete!!) additional data into the Exception object, as
the stack is unwound. By the time it gets to the final catch() block,
you cannot guarantee a particular field you depend on will be defined.
Say if your call graph looks something like this:

	main()
	  +--func1()
	      +--func2()
	      |   +--helperFunc()
	      |   +--func3()
	      |       +--helperFunc()
	      +--func4()
	          +--helperFunc()

Suppose helperFunc() throws HelperException, which func1's catch block
specifically wants to handle. Suppose func2() adds an attribute called
"lineNumber" to its catch block, which then rethrows the exception, and
func3() adds an attribute called "colNumber".

Now how should you write func1()'s catch block? You will get all
HelperException's thrown, but you've no idea from which part of the call
graph it originates. If it comes from func3(), then you have both
"lineNumber" and "colNumber". If it comes before you reach func3(), then
only "lineNumber" is defined. If it comes from func4(), then neither is
present.

So your catch block degenerates into a morass of if-then-else
conditions. And then what do you do if you're depending on a particular
field to be set, but it's not? Rethrow the exception? Then you have the
stack trace reset problem.

Whereas if HelperException always has the the same fields, the catch
block is very straightforward: just catch HelperException, and you are
guaranteed you have all the info you need.

Then if func3() wants to add more info, create a new exception derived
from HelperException, and add the field there. Then in func1(), add a
new catch block that catches the new exception, and makes use of the new
field.

This does introduce a lot of little exception classes, which you could
argue is class bloat, but I don't see how the Variant[string] method is
necessarily superior. It comes with its own set of (IMHO quite nasty)
problems.


T

-- 
One disk to rule them all, One disk to find them. One disk to bring them
all and in the darkness grind them. In the Land of Redmond where the
shadows lie. -- The Silicon Valley Tarot


More information about the Digitalmars-d mailing list