The Right Approach to Exceptions

H. S. Teoh hsteoh at quickfur.ath.cx
Mon Feb 20 16:25:57 PST 2012


On Mon, Feb 20, 2012 at 05:15:17PM -0600, Andrei Alexandrescu wrote:
> On 2/20/12 4:04 PM, H. S. Teoh wrote:
[...]
> >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.

Except that if the information you depend on is only set in the
Variant[string], then you'd have to check and rethrow.


[...]
> >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?

Not each possible control flow, but each meaningful exception type,
i.e., at some intermediate level, you catch ConversionError and throw
IntOverflowError, as you suggested. You don't need a separate type for
every possible place where ConversionError gets thrown.


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

Formatting should use class reflection. We already discussed that, and
we already agreed that was the superior approach.

When you're catching a specific exception, you're catching it with the
view that it will contain precisely information X, Y, Z that you need to
recover from the problem. If you don't need to catch something, then
don't put the catch block there.

The problem with using Variant[string] is that everything gets lumped
into one Exception object, and there's no way to only catch the
Exception that happens to have variables "p", "q", and "r" set in the
Variant[string]. You have to catch an exception type that includes all
sorts of combinations of data in Variant[string], then manually do tests
to single out the exception you want, and rethrow the rest. That's where
the ugliness comes from.


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

Again, we've already agreed class reflection is the proper solution to
this one.


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

Agreed. But it shouldn't be the be-all and end-all of data passed in
exceptions. If anything, it should only be rarely used, with most
exception classes using static fields to convey relevant information.

I can see the usefulness of using Variant[string] as a way of
"decorating" exceptions with "extra attributes", but it shouldn't be the
primary way of conveying information from the throw site to the catch
site.

As for iterating over the information in the most derived class, for
formatting, etc., class reflection is the way to go. 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? How do you format this
exception? Should the format string include extra_info1 and extra_info2,
or not? If it doesn't, what's the use of this extra info? If it does,
what happens if these fields are missing?

This is what I mean by not being able to depend on whether some data is
there. Ultimately, to do anything useful with the info in the object,
you need to know what's there. Preferably, the object's type will tell
you exactly what's there, then you do a simple map from type to list of
available attributes (e.g., map exception type to format string with
known, static list of attributes). But if the type doesn't guarantee
what data will be present, then your code becomes vastly more complex,
you have to deal with potentially all possible combinations of what's
there and what isn't. Instead of a single format string for a single
exception type, you now have a combinatorial explosion of format strings
for every possible combination of missing/present fields in the
exception object.

Just because the catch block can just blindly hand this Variant[string]
over to the formatter doesn't solve the problem. It merely moves the
complexity to the formatter, which *still* has to deal with what happens
if what the format string expects to be there isn't there.


T

-- 
Public parking: euphemism for paid parking. -- Flora


More information about the Digitalmars-d mailing list