The Right Approach to Exceptions

Andrei Alexandrescu SeeWebsiteForEmail at erdani.org
Mon Feb 20 15:15:17 PST 2012


On 2/20/12 4:04 PM, H. S. Teoh wrote:
> On Mon, Feb 20, 2012 at 03:12:08PM -0600, Andrei Alexandrescu wrote:
> I still don't like the idea of using Variant[string], though.

I don't like it, either. I mean not "like" like. It's an approach 
suggested by necessity.

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

In mine, too. Literally. We're on the same boat.

> (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.

But that's a plus. It means the approach scales up to any number of 
control flows, of which there are combinatorially many. Defining one 
type for each... well you wouldn't "like" that, either.

> By the time it gets to the final catch() block,
> you cannot guarantee a particular field you depend on will be defined.

Indeed. If you depend on anything you'd want to catch the specific type.

> 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.

Exactly. So you suggest adding one type for each possible control flow? 
Are you sure this scales beyond a toy example?

> So your catch block degenerates into a morass of if-then-else
> conditions.

No, precisely on the contrary. You catch blockS degenerate into a morass 
of catch (This) { ... } catch (That) { ... } catch (TheOther) { ... }. 
That is fine if the code in different "..." does very different things, 
but it's a terrible approach if all do the same thing, such as 
formatting. That shouldn't make anyone feel better than using a morass 
of if/else.

The code with Variant[string] does not need combinatorial testing if it 
wants to do a uniform action (such as formatting). It handles formatting 
uniformly, and if it wants to look for one particular field it inserts a 
test.

> 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.

Don't forget that Variant[string] does not preclude distinct exception 
types. It's not one or the other.

> 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.

HelperException can definitely be there. It can only help if there's 
additional information associated with it.

> 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.

They call that non-scalable code bloat.

> 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.

Variant[string] is not superior because it doesn't compete against 
anything. It's a simple addition to the primitives available to the base 
Exception class.


Andrei


More information about the Digitalmars-d mailing list