Exception Hierarchies

Allen Wirfs-Brock Allen_Wirfs-Brock at Instantiations.com
Mon Aug 16 23:17:27 UTC 1999


At 04:29 PM 8/16/99 -0400, Norton, Chris wrote:
>Hi Folks.
>
....
>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>Alan Lovejoy [sourcery at pacbell.net] wrote:
>[...snip...]
>> Ah, then perhaps the problem is simply that "Error" is misnamed.
>> 
>> I suggest the following hierarchy:
>> 
....
>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>
>I'm not an expert on Exception handling, but I sure find it handy to say:
>
>    ["some risky code"] on: Error do: ["clean up after my code died"].
>
>I think that Error is a good "catch all" name for (near-)fatal exceptions.
>Having said that, I'd like to point out that in VSE, there is an exception
>handler called KeyboardInterrupt() that happens when you hit Ctrl-Break.
>This interrupt is subclassed from Notification(), which is not subclassed
>from Error.  Perhaps the Halt and UserInterrupt should be subclassed from
>Notification() in Squeak.  After all, they aren't really errors.  They are
>simply interruptions that can (usually) be resumed.
>
....

I designed the VSE exception handling system and specified the ANSI
Smalltalk exception system so I suspect that some of my experience may be
relevant to this discussion. I have just a few points that may be helpful:

"NoHandlerSignal" vs #defaultAction
One of the most significant differences between the VisualWorks exception
system and the VSE/ANSI exception systems is what occurs if no explicit
handler exists. The VSE/ANSI systems does not really have the concept of an
"unhandled exception". Every exception class implements or inherits a
#defaultAction method that is used to handle the exception if no explicit
handler is found. Most exceptions are defined to do something reasonable as
their #defaultAction. Error and its subclasses should open an error
notifier. Notification does nothing but resume execution. Because of the
default actions there is really little need for a "NoHandlerException". If
you really wanted one I would implement it in the #defaultAction method of
Exception. Perhaps something like:

	defaultAction
		"nobody handled the exception, turn it into a NoHandlerException"
		self resignalAs: (NoHandlerException new originalException: self)

In this case NoHandlerException darn well better over-ride #defaultAction
to do something different then the above.

Its not clear to me why you might want to introduce NoHandlerExceptions
into this system. The first thing that comes to mind would be for some sort
of debugging support where you wanted to perform special handling for Error
exceptions. For this type of thing I think a better solution might be to
some sort of pluggable mechanism for changing the default behavior.

Inadvertently catching default actions.
When an exception hierarchy is filled out with default actions, the
unintentional catching of exceptions can introduce subtle bugs. Here is a
real life example:

You are implementing FileStreams using native OS file operations. In the
native interfacing level you define a subclass of Error, NativeIOError,
that is signaled whenever a system call fails. One of the failure
conditions is end-of-file so the defaultAction for NativeIOError is defined
to resignal a StreamAtEnd exception in that particular case. A database
subsystem then used FileStreams and explicitly deals with StreamAtEnd in a
manner that is transparent to the clients of the database subsystem.
Somebody writes some code that uses the database subsystem and it works
fine. Then, they decide they want to control how errors are reported to
users so they wrap the database client code like this: [perform a
transaction] on: Error do:. As soon as they do this, the system begins to
fail with mysterious NativeIOErrors. Why? Because their Error handler was
catching the NativeIOError and thus preventing its #defaultAction from
taking place and hence the NativeIOError never got converted into a
StreamAtEnd for the database subsystem to deal with.

There are several lessons to learn from the above. First, Error is intended
for "end user" programmer use. It's perfectly reasonable for somebody to
code [...] on: Error do:. If you are implementing "system" code, don't
depend upon defaultActions that might be over-ridden by an user's  handler
for Error. If you want to use this implementation style, create your own
exception hierarchy subclassing Exception instead of Error.

Don't define handlers for Exception or Notification
You almost never want to define an explicit handler for either of these
Exceptions. They exist primarily to provide default behaviors within the
exception implementation hierarchy. Here's an example of what can go wrong:

Somebody was building a QA test harness for Smalltalk programs. They wanted
to log any "unexpected" exceptions that occurred during a test. So they
wrapped every test with code something like:

	[aTest value] on: Exception do: [:e| e printOn: Transcript]

Some tests that worked before stopped working because default actions were
no longer executing. This is probably one of the few cases where handling
Exception is actually a reasonable thing to do. To make it work correctly
they should have coded it as:
		[aTest value] on: Exception do: [:e| e printOn: Transcript. e pass]

Don't overuse exceptions.
The ANSI/VSE or VW exception mechanisms are very powerful. They come very
close to being Turing complete. Some people, after learning these systems,
will try to use them for everything, even to implement their core
application logic. Just because something is possible does not mean it is a
good idea. Use exceptions sparingly to deal with exceptional situations.
Don't overuse them.

Exceptions may not be the best solution.
Even in places where exceptions seem like a good fit, they may not be the
best solution or provide the best performance. #at:ifAbsent: could be
rewritten/replaced using a KeyNotFound exception. If you do this you will
probably find that you program just got slower.

Keep exception hierarchies simple.
Its much easier to define deep and broad exception hierarchies then it is
to use them. I've seem many projects where time was spent defining complex
exception hierarchies and then discovered that nobody ever caught anything
except the exception at the top of the hierarchy. Its just too hard to
remember dozens of names of various exceptions and the conditions under
which they are signaled. Most people will just handle Error. Only define
separate Exception classes if you expect special handlers for them to be
frequently defined. ZeroDivide and StreamAtEnd probably meet this criteria,
most other arithmetic or "I/O Errors" don't. In reviewing designs I've
frequently found "wasted" exception classes, I've almost never found too few.

KeyboardInterrupt
Or whatever you want to call it. Make it either a subclass of Notification
or a new top level (subclass of Exception) exception. Don't make it an
error, for various reasons explained above.

Cluttered call stack
The implementation of an exception handling system is likely to introduce
all sorts of implementation artifacts that will show up as activation
record in the method call stack of the debugger. This can be very confusing
and tend to obscure the high level control flow of the program. Most
programmer shouldn't have to see these things. In VSE we normally filtered
out these artifacts so they weren't visible in the debugger. I would highly
recommend that same for Squeak.

Allen_Wirfs-Brock at Instantiations.com





More information about the Squeak-dev mailing list