Exception handling


Early versions of Smalltalk did not have exceptions, and provided no particular support for error handling. Exception handling schemes which by-pass the normal flow of control, and which use objects to represent error conditions, have a number of advantages over other error handling techniques, such as testing return values:

  1. Error handling code can be separated from normal code. In particular the frequent and repetitive testing of return values, and propagation of error return values, is not required. Since exceptional conditions can normally be ignored, code can be less complex, less likely to contain bugs, and easier to read.
  2. Error return values can be ignored, exceptions cannot. In practice lazy or pressurised programmers frequently do not test for failure codes.
  3. The range of return values may not include any space for error values, hence the rather confusing technique (used in OLE) of returning values through "out" parameters and using the return value for success and error codes only. Exceptions by-pass the stack, and so there is no possibility of conflict.
  4. The point at which an error occurs is rarely a suitable place to handle it, particularly in library code, but by the time the error has been propagated to a place where it can be handled, too much contextual information has been lost. Exceptions bridge this gap - they allow specific error information to be carried from the point where it available to a point where it can be utilised.
  5. Resumable exception models can be used to construct resilient systems which carry out repairs when exceptions occur, with subsequent resumption of execution as if nothing had happened. For example, this technique is useful for on-demand resource allocation.
  6. Using exceptions may be more efficient because the normal execution path does not need to test for error conditions..

The end result of all these advantages, is, hopefully, faster development of more robust and easier to maintain systems. Consequently exception handling schemes have found there way into more recent versions of Smalltalk, and Dolphin is no exception!

Class vs Instance

There are two broad categories of exception handling systems in Smalltalk implementations:

  1. Class based, as exemplified by Visual Smalltalk™
  2. Instances (or Signal) based, as exemplified by VisualWorks™

Although the instance based mechanism does have a slight space advantage (no new classes are needed), this is outweighed by the advantages of a class based mechanism, such as the ability to add state and behaviour. In fact the exception handling specified in the draft ANSI standard for Smalltalk (X3J20) is based on that of Visual Smalltalk, and the most natural implementation is with classes. Dolphin has a class based exception mechanism which is compliant with the draft standard, but also implements an instance based mechanism on top of this (Signal).

In Dolphin's class based exception handling implementation, exceptions are represented by subinstances of the class Exception. Specific types of exception are instantiated and signalled (e.g. by sending the #signal: message to the class), after which user defined exception handlers can catch (typically by specifying the relevant class or classes of exception) and handle them. Exception handlers are normal Smalltalk blocks, but can also affect the execution state (e.g. to unwind the stack up to that point, or to continue execution as if nothing had happened) by sending messages to the exception itself which is passed to the handler block as an argument. Since you are free to define new classes of exception, the exception can include whatever information about the error condition that you require, and you can also define methods to add appropriate behaviour..

Resumable vs Non-resumable Exceptions

An important feature of the Smalltalk exception handling model is that it supports resumption, i.e. the exception handling code can specify that execution is to continue from the point at which the exceptional condition was detected. This is in contrast to the C++ exception handling model in which execution can only be continued in the immediately enclosing scope of the handler.

The ability to resume after an exception is a very powerful concept: For example it can be used to implement a deferred resource allocation scheme. Resumable exceptions reduce the need explicitly check for recoverable failures, since the recovery can be performed in the handler, before continuing execution from the point it left off. Such a powerful capability must be applied with care if it is not to result in spectacular failure. The danger inherent in resumption is one of the reasons put forward for the non-resumable model used in C++, but another reason is that it is difficult to implement efficiently in a non-reflective programming system.

Not all Exceptions are resumable, with resumability usually being an attribute of the particular class of Exception.

General Categories of Exception

The Exception hierarchy divides into three broad categories:

  1. Errors
  2. Warnings
  3. Notifications

There is a corresponding class for each of these which represent the most general forms of each category of exception. These 3 exception classes can be caught and raised (though in the case of errors in particular, this is not good practice).

Errors

Errors represent representing exceptional conditions which are normally considered 'fatal'. Errors are not normally resumable, though specific subclasses can be resumable if desired, an example being MessageNotUnderstood.

The default action for an unhandled error is to send an #onUnhandledError: message to the SessionManager. The development system session manager, DevelopmentSessionManager, implements this by popping up a walkback. The standard RuntimeSessionManager displays a message box, and then proceeds as specified by the user (there will be no choice if the Error is not resumable).

Applications, which typically implement their own session manager to meet their own specific requirements, can provide alternative behaviour by overriding the #onUnhandledError: message appropriately.

The most general form of error exception is represented by the class Error, and instances of Error represent non-resumable exceptional conditions with an associated description. Error is an appropriate exception to raise where the condition is the result of an error which cannot be recovered from (e.g. a violation of a basic invariant), but it is not specific enough for situations where recovery is desirable.

The majority of more specific exception classes will be found under Error, and this is where you are most likely to want to introduce your own exception classes.

Warnings

Warnings represent resumable exceptional conditions which are non-fatal. Resuming after an unhandled warning is usually successful, though the user normally has the option of cancelling the operation (though this will depend on the session manager, which once again is notified of unhandled warnings via the #onUnhandledWarning: message).

The most general form of warning is represented by the class Warning, which like Error contains a displayable description. You are unlikely to need to subclass Warning.

Notifications

Notifications represent resumable exceptional conditions which are non-fatal, and informational only. By default these are sent to the trace device if not handled, though once again this is up to the SessionManager, which can override #onUnhandledNotification: as appropriate. On a development system, the trace device is the transcript. The default run-time trace device is the kernel provided one (i.e. that to which output is written by OutputDebugString()).

The most general form of notification is represented by the class Notification, which like Error contains a displayable description. You are unlikely to need to subclass Notification.

Raising Exceptions

Having detected that a particular exceptional condition has occurred, you will need to instantiate and raise an instance of the appropriate Exception class. The easiest way to do this is by using the class #signal: method. For example:

Error signal: 'A gratuitious error'

The draft ANSI standard specifies that the argument to #signal: should implement the <string> protocol, but Dolphin will actually accept any object to which it sends #displayString in order to generate the error message text.

If you need to supply additional information when raising an exception, then the object can be instantiated in the normal way, and once set-up with the use of accessor methods, it can be sent #signal/signal: to actually raise it. For example BoundsError is an error class which includes the receiver whose bounds were violated, and the index at which the violating attempt was made. BoundsErrors are raised as follows:

^BoundsError new
	receiver: self;
	signal: anInteger

Bear in mind that you are free to add whatever behaviour and state you need in your own exception classes.

Catching Exceptions

Protecting a sequence of Smalltalk statements and catching a class of exception, or classes of exception, that occur while executing that sequence is achieved by wrapping the statement sequence in a block (called the protected block, or try-block), and sending the block a #on:do: message to establish a new exception context. The two arguments to #on:do: are the class of Exception to be caught (but catching multiple exceptions is possible), and a monadic block to handle exceptions which are of that kind.

For example:

ByteArray>>safeAt: index
	"Answer the byte at the specified index in the receiver.
	If the index is out of bounds, answer 0"

	^[self at: index] on: BoundsError do: [:boundsError | 0]

Try browsing references to #on:do: to see some real examples in the base system.

Catching Multiple Exceptions

When you establish an exception environment with #on:do: and specify a class of exception to catch, then the handler will be evaluated when any exceptions are raised which are subinstances of that class. Sometimes you may want to catch a group of exceptions which are not necessarily related by hierarchy, and this is where ExceptionSets come in.

ExceptionsSets are normally constructed by specifying a comma separated list of Exception classes, for example:

ExternalStructure>>safeAt: index
	"Answer the byte at the specified index in the receiver.
	If the index is outside the bounds of the buffer owned by the receiver,
	or not accessible in a referenced buffer, then answer 0."
	^[contents at: index] on: BoundsError, GPFault do: [:ex | 0]

An ExceptionSet can contain as many classes of Exception as you wish, and if any are the class, or a superclass, of an exception raised in the protected block, then the handler will be evaluated.

On other occassions, particularly on subsystem boundaries, we may need to handle a number of different possible exceptions separately. This is achieved with ExceptionHandlerSets, usually by using on of the #on:do:[on:do:]+ messages to BlockClosure. For example:

ExternalStructure>>safeAt: index
	"Answer the byte at the specified index in the receiver.
	If the index is outside the bounds of the buffer owned by the receiver,
	or not accessible in a referenced buffer, then answer 0."
	^[contents at: index]
		on: BoundsError, GPFault do: [:ex | 0]
		on: Error do: [:ex | Notification signal: 'Unexpected error ', ex description. 0]

The more general classes of exception should be specified last, as any more specific classes/sets appearing later will otherwise not get a look in.

The base system includes messages for 1 to 4 separate exceptions and handlers, though more can be accomodated by adding more messages, or by manually constructing an ExceptionHandlerSet and passing it to BlockClosure>>onDo:. You can browse BlockClosure>>on:do:on:do: to see how to build an ExceptionHandlerSet.

Handling Exceptions

Having caught an exception (hopefully one you have anticipated), you then need to do something with it to recover from the condition. Of course the handler block can contain any sequence of Smalltalk statements you require, but it is passed the exception instance as its argument when evaluated, and Exceptions understand a 'handler response' protocol which can be used to affect the execution state. The messages and their meanings are:

Note that some of these handler responses are quite powerful, and dangerous (especially #retry(:)), and should be used with caution.

The default behaviour if no handler response is specified is ALWAYS to continue execution with the statement immediately after the handler block (i.e. it is the same as if #return: had been explicitly specified), regardless of whether the exception is resumable or not, and is very much like the C++ exception handling model. You may find it clearer to explicitly send #return: to the exception, with the desired result as the argument. In order to resume after a handled resumable exception, it is necessary to explicitly send #resume(:) or #exit(:) to the exception in the handler block.

Dolphin beta-1 implemented the behaviour from an earlier version of the draft standard, which specified that the default where no handler response was explicitly specified depended on whether exception was resumable or not. This behaviour was changed because it was found to be confusing.

Further exceptions may occur inside handler blocks, and these can be caught and handled too. Handler blocks are evaluated in the exception enviroment current when they were constructed, which in practice coincides with the behaviour one expects.

It is worth bearing in mind that when an exception handler block is being evaluated, that the stack has not yet been unwound (if it had then resumption would not be possible). The stack is only unwound when the handler completes by dropping off its end, or by some explicit handler response message being sent to the exception.

Exception Hierarchies

An advantage of the class based exception mechanism used in Dolphin, is that hierarchies of exceptions can be defined, and the more specialised exceptions can be caught and handled by handlers written for their superclasses, allowing you to exploit the polymorphic behaviour of the specific instances from a generic handler. However, deeply nested hierarchies of increasingly specialised exceptions, may well prove overly complex, and be more trouble than they are worth. Subtle distinctions may not be that useful in the handlers since one generally wants to minimise the amount of error handling code, and catch and handle the most generic level of error which permits recovery without suppressing unexpected errors.

The most important consideration when adding new exception classes is that they should be sufficiently specific to enable identification and handling of the error at the correct level. You may wish to introduce one generic error class, and quite specific subclasses of that as the need arises. An example of this can be seen in the base system in the HostSystemError hierarchy. HostSystemError is raised when some error calling an operating system function is encountered, but there are also very specific subclasses such as OutOfMemoryError and GPFault to handle specific host system errors. Specific error instances can be raised off the more generic class when appropriate, and this does make it easier to add new specific exceptions.

If you can identify situations where you do wish to group the handling of specific exceptions, then there may be a case for introducing another level in the hierarchy. However it is worth remembering that exceptions need not necessarily be related by hierarchy to be caught and handled by the same handler, since there is a mechanism for catching specified sets of exceptions (and you could quite easily add your own mechanism for determining catchers too). Furthermore because Smalltalk does not rely on inheritance relationships for polymorphic behaviour, as long as all the handled exceptions support the necessary protocol, then there is no requirement for them to share any common ancestor except Exception itself.

Exceptions vs. Unwinds

Unwind blocks are the blocks passed as arguments to the BlockClosure>>ifCurtailed: and BlockClosure>>#ensure: methods, and are typically intended to perform clean-up operations on behalf of the receiving block. #ensure: blocks are guaranteed to be run, whether the receiving block exits normally or not. #ifCurtailed: blocks are run only when the receiving block exits abnormally, as a result of a ^-return, an exception, or process termination. Unwind blocks are very useful for maintaining a valid image state (e.g. for unlocking mutual exclusion Semaphores).

When an exception occurs, the Process stack is unwound after the exception is handled, possibly terminating the process if there is no handler. As the process stack is unwound, and unwind blocks encountered along the way are evaluated. As with stack unwinding which occurs as a result of performing a ^-return from a block, any unwind blocks which are encountered are evaluated.

To some extent, exception handler blocks (particularly those which catch Error) can be used as a form of unwind block, but there is a distinction which must be understood: Following the evaluation of an exception handler block, if that block does not perform some explicit handler response, then the unwinding of the stack is terminated at that point, and execution continues immediately after the handler block. In contrast, unwind blocks only momentarily interfere with the progress of the stack unwinding to the return destination. Additionally, exception handler blocks are not evaluated as a result of ^-returns.

For example, consider a more guarded version of Object>>printString, designed to suppress errors occurring in #printOn:

safePrintString
	"Answer a String whose characters are a description of the receiver as a developer
	would want to see it."

	| stream |
	stream := WriteStream on: (String new: 10).

	"Catch attempts to print an invalid object which might otherwise
	throw an exception"
	[self printOn: stream] ifCurtailed: [
		stream 
			reset;
			nextPutAll: 'an invalid ';
			display: self class].
	"...but not to here"

	^stream contents

Although this method would print the 'an invalid XXX' message should an error occur in the #printOn: method, it would not prevent a walkback. #ifCurtailed: is an unwind handler which guarantees that the argument block (and in this case it must be a block for ANSI compatibility, though the Dolphin implementation will work for other niladic valuables too) will be evaluated, if the receiving block (and it must be a block) should attempt to exit directly from the method. It does not prevent the further propagation of the error. There are three circumstances in which this might happen:

  1. The receiver block itself includes a ^-return (i.e. return from home method context).
  2. A block is evaluated inside the context of the receiver block which itself performs a ^-return from its own home method context, where that home method context encloses the receiver block (i.e. it is further down the stack).
  3. An exception is raised.

All of these cases boil down to some attempt to perform a "far" return which returns to some method an arbitrary depth back down the stack. In C++ it is only possible to do this with exceptions (or a nasty long jump). In Smalltalk exceptions are actually implemented by making use of the ability of blocks to do "far" returns - i.e. case 3 is actually a clever use of case 2.

The other important thing to know about #ifCurtailed:, is that it does not prevent the "far" return from happening - i.e. it does not cause execution to continue from the statement after its argument block. In order to do that, you must use exception handling (i.e. use #on:do: instead of #ifCurtailed:). The intention of the #safePrintString example above should actually be expressed as:

...
[self printOn: stream] on: Error do: [ :e |
	stream 
		reset;
		nextPutAll: 'an invalid ';
		display: self class].
"Will always get to here, even if #printOn: throws an Error"
...

Note also that unwind protection blocks are not run when the exception mechanism is searching for a handler, only when a handler is actually found and evaluated. If there are no handlers in the call stack, then a walkback will appear, and the unwind protection blocks will not at that stage have been run. They will not be run until you press Terminate or Kill (though this may change in a future release, because it has the disadvantage that the system may be left in an invalidate state when the walkback appears).

#ensure: is similar to #ifCurtailed: (and is implemented using the same mechanism - see the methods in BlockClosure), but always evaluates the argument block.

In summary, unwind protection (#ifCurtailed:, #ensure:) is not exception handling. If you want to guarantee that certain operations are always performed (cleaning up after yourself), but do not want to actually intercept and handle exceptions (i.e. you don't want to prevent a walkback), then unwind protection is what you want. If, on the other hand, you actually want to handle the errors and continue, exception handling is what you want.

Win32 Structured Exceptions

The Dolphin VM catches a number of Win32 exceptions (the notable examples being GP faults, and floating point errors) and notifies the image of there occurrence.

The VM catches the Win32 exceptions even if they did not occur in Smalltalk code, but happened in some external library function. For example, if you evaluate:

	CRTLibrary default sqrt: -1

Then you will get a walkback resulting from an invalid floating point operation exception which occurs in the C-runtime library's sqrt() function.

The Dolphin exception classes which are created when Win32 exceptions occur are:

Note that although access violations can often be recovered from by dismissing the walkback (especially if they arise because of attempts to read inaccessible memory, or read/write through a null pointer), they may represent just the tip of the iceberg. A GPFault may only be raised after everything has gone horribly wrong, so do not be too surprised if Dolphin subsequently crashes. If you find that regular GPFaults occur during garbage collection activity, then do not save your image, as it has been corrupted (perhaps by an external function writing off the end of an inadequately sized buffer).

A limitation is that although Win32 structured exceptions can often be resumed, the Dolphin equivalents cannot.

How it works

The exception handling system in Dolphin is written entirely in Smalltalk (though it does rely on the VM's special support for unwind blocks), and is a good example of the systems reflective capabilities - because even the execution state of a program is accessible to that program (as objects of course), it is possible for a program to modify its execution state, and this is precisely what the exception handling system does.

It is not necessary to understand precisely how exceptions work in Dolphin, but an overview may be helpful in when using them, and will give you some idea of the overhead involved. If you want to know more about the implementation of exceptions in Dolphin, you can browse through the source code in Exception (and its immediate subclasses),

Each Dolphin process maintains a stack of exception contexts, with a new one being instantiated and pushed on the stack for each #on:do:. The exception context includes all relevant details from the #on:do: message, and from the execution state at the time of the message. As #on:do:'s exit, the corresponding exception context is popped from the stack. Note that establishing an #on:do: handler carries the overhead of instantiating an exception context. A similar overhead is present in the implementation of the native "structured exceptions" on Win32 platforms, for example in C++, except that the implementation details are hidden from you by the compiler. This overhead is incurred regardless of whether any exceptions are actually raised.

An exception is raised by instantiating a suitable instance of an exception class with pertinent details relating to the exceptional condition (e.g. in the case of a BoundsError, the receiver and the index which is out of bounds), and sending it the #signal message (though frequently this is wrapped inside a shortcut instance creation method provided by the exception class).

When an exception is raised in a Process, execution is effectively suspended (though only in the same way that any method execution is suspended when it sends a message and another method is invoked), and the exception system begins a search through the stack of exception contexts. At each level, the exception context is queried to see if it is suitable for handling the raised exception. Typically this matching operation is simply an #isKindOf: test to see if the class of the raised exception is the same kind as the class of exception specified by the handler, but more sophisticated matching is possible (for example against a set of Exception classes). The stack will tend to be only a few contexts deep, so this is a very fast search, but the overhead relative to the alternative of explicitly checking for an error is high, although it is only incurred if the exceptional case.

If an exception context is located which wants to handle the exception, then the corresponding handler block is evaluated with the exception instance as the argument. The handler block can

Guidelines for Use

As discussed in the introduction, exceptions have a number of advantages over alternative methods of reporting exceptional conditions (e.g. returning a distinguished value), and where at all possible should be used in preference to those alternative methods. The rule-of-thumb is: Use exceptions for exceptional conditions only. If a particular condition is expected to occur in normal execution, then it should probably not result in a raised exception.

In cases where you expect an error condition to arise frequently (e.g. 'not found' type errors), this should probably be handled by allowing the user to specify a block to be evaluated when that condition arises. The usual practice is to then provide a simpler wrapper method which invokes the other, supplying a block which raises an exception (e.g. see Dictionary>>at:ifAbsent: and Dictionary>>at:).

As discussed in the implementation overview, raising and catching exceptions carries certain overheads:

Overhead should not prove to be an issue if exceptions are used appropriately, because, by definition, they are intended to represent exceptional conditions which happen infrequently, and which are, therefore, not part of mainstream execution.

Try not to design deeply nested hierarchies of exceptions, since the additional complexity resulting is rarely necessary. One cannot necessarily predict the exceptions that "user" code will want to catch and handle together, and carefully constructed taxonomies may prove inappropriate. Instead add specific exception classes which capture all the necessary information, and group these under a superclass only where there is a clearly identifiable need to catch and handle that group of exceptions with single handlers.

The exceptions raised by a method are an important part of its specification, since without this information one cannot handle specific errors. At the very least the exceptions which could be raised as the result of the invocation of a public method should be documented.

Resist the temptation to catch quite abstract classes of exception to save on programming. In order that exception handling code can usefully recover from specific and expected exceptional conditions, you should catch the most specific class of exception that you can which enables you to recover from the exception. In particular never catch the abstract class Exception. If you catch broad classes of exception, then you may end up suppressing exceptional conditions which you haven't actually handled. This can make systems difficult to debug in development, and can lead to undetected data corruption in production systems.

You may find yourself coding handler blocks which examine some detail of a caught exception to determine whether it should really have been caught in the first place. Indeed this is quite common practice in one existing Smalltalk implementation, which even compares against the message text in the exception. However, it is bad practice, defeating the object of a flexible class based exception handling mechanism. In this case the correct thing to do is to subclass the generic class of error, and then raise and catch the more specific subclass.

Groups of unrelated exceptions can be caught by using ExceptionSets, so they do not need to be related by hiearchy. You can even design your own exception catchers, by implementing the ANSI protocol, exceptionSelector.

Multiple classes (or sets) of exceptions can be caught from the same try block and directed to separate handlers by making use of the BlockClosure>>#on:do:[on:do]+ series of messages. Listing multiple exception classes and handlers is the corect thing to do instead of specifying an ExceptionSet and then switching on the type of the exception in the handler block.

Raising and handling exceptions disrupts the normal flow of control. It also separates the point of detection of an exceptional condition, from the point where that exception is dealt with. These, useful, features can make it more difficult to determine the behaviour of a system by browsing the source code. Resuming after an exception is non-obvious (and also subject to further failures). Retrying after an exception is particularly esoteric technique and should be used sparingly.

In general Errors should not be resumable, since they are supposed to represent fatal conditions, which if resumed, would almost certainly result in further exceptions. In specific circumstances where recovery is possible, but the exception is in other respects and error condition, you may be able to make a case for permitting resumption.

In summary:

  1. Exceptions should be raised in preference to returning distinguished error values which must be tested, but ...
  2. ... use exceptions for conditions which are truly exceptional, not for frequently expected errors.
  3. Raise exceptions as soon as possible after detecting an exceptional condition so that all pertinent information relating to the condition can be included.
  4. Catch exceptions at a point where it is possible to handle them effectively.
  5. Do document which exceptions public methods might raise.
  6. Avoid establishing exception handlers using #on:do: inside tight loops, or other frequently executed code, if you are concerned about performance.
  7. Don't over engineer your exception hierarchies - keep them shallow and simple.
  8. Never catch Exception.
  9. Think twice before catching Error, it is rarely appropriate outside development tools.
  10. If you must catch Error, provide detailed logs so that unexpected conditions are not missed.
  11. Examining the detail of an exception inside a handler block to see whether it can be handled is an indication that too generic a class of exception was raised and caught, and that further specialization may be necessary.
  12. Don't switch on the type of exception in a handler block, instead use multiple catcher and handler pairs established with the BlockClosure>>#on:do:[on:do:]+ series of messages.
  13. Avoid #retry and (especially) #retryUsing:.
  14. Avoid defining resumable Errors.