The Right Approach to Exceptions

H. S. Teoh hsteoh at quickfur.ath.cx
Sat Feb 18 18:00:06 PST 2012


On Sat, Feb 18, 2012 at 05:47:58PM -0600, Andrei Alexandrescu wrote:
> On 2/18/12 1:41 PM, H. S. Teoh wrote:
[...]
> >It's only useless because of a poorly-designed exception hierarchy.
> >Java, for example, has useful things like FileNotFoundException,
> >DiskFullException, etc., that you can catch to handle specific
> >problems in a specific way. They are also part of a well-designed
> >hierarchy. For example, both of the above exceptions are subsumed
> >under IOException, so if you wanted to handle a general I/O exception
> >and don't care which one it is, you could just catch IOException.
> 
> It's great that you bring expertise from the Java world.

You flatter me, but I should make it clear that I'm no Java expert. I
haven't used it in a significant way for years. But I do know enough
about it to know, at least in principle, the merits of its exception
class hierarchy.


> I should note that the above does little in the way of putting an
> argument to the table. It appeals to subjective qualifications
> ("poorly designed", "well designed") so one's only recourse to getting
> convinced is just believing what you say without any evidence. It's
> equally unimpressive that FileNotFoundException and DiskFullException
> inherit IOException; seeing that one might say "yeah, sure" but there
> should be some compelling evidence that the distinction makes a
> difference.

I wanted to talk about the principles of using an exception hierarchy,
not get lost in the details of it, that's why I didn't go into the
details of how exactly the hierarchy should be designed.

As for this particular case, I was only trying to illustrate the point
with a concrete example; I wasn't suggesting that we must implement
those exact three exception classes. Nevertheless, I would say the
decision to make FileNotFoundException and DiskFullException inherit
from IOException can be based on:

1) The fact that they are based on a common set of OS primitives, i.e.,
the filesystem, thereby providing a logical set of related exceptions;

2) It makes sense to put errno in IOException, whereas it doesn't make
sense to put errno in Exception, since not all Exception's have any
relation to errno.

3) From a more abstract POV, it makes sense to group file-related
exceptions under a common root, command-line parsing exceptions under
another root, numerical exceptions under yet another root, etc.. And how
do we decide which exception belongs where? By checking whether an
application might want to catch all exceptions in a group as a whole,
e.g., a numerical app wants to handle any numerical errors, but don't
care about I/O errors (which should just propagate). So it wouldn't make
sense to put, say, ArithmeticOverflowException under IOException.


> >Now, granted, there are limitations to such a design, for example if
> >you want to catch a category of exceptions that don't map precisely
> >to a node in the inheritance hierarchy. It may be possible to deal
> >with such situations by making Exception an interface instead,
> >although that may introduce other issues.
>
> From this and other posts I'd say we need to design the base exception
> classes better, for example by defining an overridable property
> isTransient that tells caller code whether retrying might help.

Just because an exception is transient doesn't mean it makes sense to
try again. For example, saveFileMenu() might read a filename from the
user, then save the data to a file. If the user types an invalid
filename, you will get an InvalidFilename exception. From an abstract
point of view, an invalid filename is not a transient problem: retrying
the invalid filename won't make the problem go away. But the application
in this case *wants* to repeat the operation by asking the user for a
*different* filename.

On the other hand, if the same exception happens in an app that's trying
to read a configuration file, then it *shouldn't* retry the operation.

The bottomline is, you can't expect a couple of properties in the base
class (most generic level) will be helpful in all cases. In some cases,
you *want* to know exactly what the exception was programmatically, so
that the program knows how to react properly.


[...]
> >The problem with this approach is the proliferation of of exception
> >classes, many of which differ only in fine ways that most
> >applications wouldn't even care about. The basic problem here is that
> >we are mapping what amounts to error codes to classes.
> 
> Yes. Types, to be more general. The question is why are various error
> deserving of their own types.

I have to say, I'm not wedded to the idea of Exception class
hierarchies, but currently it's the best choice on the table.

Basically you *need* some kind of polymorphic object to represent an
exception, because errors come in all shapes and forms, and it's simply
not good enough to have what amounts to a binary system (fatal
uncatchable exception and non-fatal exception that you may catch and
attempt to continue if you dare). Whether it's a class, a template, or
whatever, doesn't really matter in the end, but you do want to have:

1) Genericity: if I don't care what error it is, just that an error
happened, I should be able to catch exceptions in general and deal with
them.

2) Specificity: if I have a very precise error (or a very precise set of
errors) that I can handle, I want to be able to catch those, and only
those, errors, and deal with them. The ones I can't handle, I want to
just propagate.

3) Programmatic information: the fact that I want to handle a certain
set of errors means that I know how to deal with them, provided I have
enough information about them. Which means the exceptions I catch need
to come with domain-specific information (such as errno for OS-related
exceptions, or Oracle error numbers for the Oracle API library, etc.).
Having toString() as the only way to extract information out of the
exception is not good enough for recovering from errors
programmatically. (And obviously sticking errno, Oracle errors, etc.,
into a giant union in Exception isn't the right solution either.)

Currently, an elaborated exception class hierarchy, for all its warts
and the objections against it, best fits the bill.  If you have a better
way of doing this, I'm all ears. But the current "fatal"/"nonfatal"
dichotomy is simply too coarse to be of use in large applications.


T

-- 
Food and laptops don't mix.


More information about the Digitalmars-d mailing list