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