GemStone Smalltalk implements the ANSI exception handling protocols, with provisions for signaling that an exception has occurred and for defining handlers for signaled exceptions.
The Exception Class Hierarchy
describes the exception class hierarchy, listing the subclasses that correspond to events that you may want to handle.
Signaling Exceptions
describes the mechanism whereby an application can signal that a some notable event occurred. The class of the signaled exception determines which handler(s) will be invoked. A handler might halt execution and report an error to the user.
Handling Exceptions
describes how to define handlers in your application to cope with signaled exceptions. Depending on the type of the exception, your application might be able to handle the exception gracefully, possibly even without the user being informed of the exception.
The Legacy Exception Handling Framework
describes the legacy exception handling framework.
GemStone/S 64 Bit supports the ANSI Exception framework. The ANSI Exception framework defines subclasses to match the granularity of errors that you may want to handle.
GemStone also supports a Legacy Exception framework, for compatibility with earlier versions of Gemstone. This can be used to signal and handle ANSI exceptions. The Legacy Exception framework is described under The Legacy Exception Handling Framework.
Figure 14.1 shows the ANSI exception handler class hierarchy.
AbstractException ( gsResumable gsTrappable gsNumber currGsHandler
gsStack gsReason gsDetails tag messageText gsArgs )
Exception
ControlInterrupt
Break
Breakpoint ( context stepPoint )
ClientForwarderSend ( receiver clientObj selector )
Halt
TerminateProcess
Error
CompileError
EndOfStream
ExternalError
IOError
SocketError
SecureSocketError
SystemCallError ( errno )
GciErrorGciLegacyErrorGsMalformedQueryExpressionError
GsQueryExpectedImplicitIdentityIndexError
GsQueryParseError
ImproperOperation ( object )
ArgumentError
ArgumentTypeError ( expectedClass actualArg )
CannotReturn
LookupError ( key )
OffsetError ( maximum actual )
OutOfRange ( minimum maximum actual )
FloatingPointError
RegexpError
IndexingErrorPreventingCommit
InternalError
GciTransportError
LockError ( object )
NameError ( selector )
MessageNotUnderstood ( envId receiver )
NumericError
ZeroDivide ( dividend )
RepositoryError
SecurityError
SignalBufferFull
ThreadError
TransactionError
UncontinuableError
UserDefinedError
Notification
Admonition
AlmostOutOfMemory
AlmostOutOfStack
RepositoryViewLost
Deprecated
FloatingPointException
GsUnsatisfiableQueryNotification
InterSessionSignal ( sendingSession signal )
ObjectsCommittedNotification
TransactionBacklog ( inTransaction )
Warning
CompileWarning
TestFailure
ResumableTestFailure
ANSI Exceptions are class-based: you use a class in the Exception hierarchy to describe errors and other exceptions in your GemStone Smalltalk programs.
You can extend the built-in exception types by defining new subclasses. You can also change your new exception’s default behavior by adding method overrides to the new class (for example, defaultAction and isResumable).
The ANSI exception handling framework provides for zero or more dynamic (stack-based) handlers and a list of zero or more default handlers, ordered in the sequence they were installed.
When an application sends a message of the form:
Exception signal: aString
GemStone Smalltalk creates an instance of the signaled class and performs the following search for a suitable handler:
1. Search the stack for a handler associated with the exception class. In a dynamic
(stack-based) handler (described here), you explicitly identify a block of application code that might signal an exception to which you wish to respond.
2. Search the default (static) handlers. A default handler (described here) is invoked if a dynamic handler is not found or if the last dynamic handler passes the exception.
3. Search the exception class for an implementation of the instance method defaultAction. Some exception classes redefine this method, thereby establishing a handler to use in the case that there is no suitable dynamic or default handler or if the last such handler passes the exception. For example, with Notification, the default action is to ignore the exception.
If the exception class does not override the implementation of defaultAction in class AbstractException, halt the GemStone Smalltalk interpreter and pass the exception back to the client to be handled (by Topaz, GemBuilder, or another application) as an error.
Other than a few fatal errors, most signaled exceptions can be handled in your GemStone Smalltalk application. To do so, you identify the type of exception that might be signaled (Exception or, more often, a subclass of Exception) and provide GemStone Smalltalk code to handle the exception.
GemStone Smalltalk allows you to define two kinds of exception handlers: dynamic (stack-based) handlers and default (static) handlers.
A dynamic (stack-based) handler is associated with an executable block (instance of ExecBlock) and the associated state in which the GemStone Smalltalk virtual machine is presently executing. These handlers live and die with their associated blocks—when the block is exited, the handler is gone.
A dynamic handler is associated with exactly one ExecBlock and applies as long as the ExecBlock is being executed. Because an ExecBlock can be embedded in another ExecBlock (either directly or via another method), multiple dynamic handlers can be active at one time. Figure 14.2 illustrates this relationship.
To define a dynamic handler for an ExecBlock, send the on:do: message to the block. Example 14.2 defines an averagePay method for the Employee class. The method calculates an average by dividing two values. If the division signals a ZeroDivide exception, the exception handler returns zero as the result of the method. In this implementation, the method will never result in a “division by zero error” being seen by the user. (Of course, there are other ways you might write this particular method. This example simply serves to highlight the on:do: exception handling approach.)
method: Employee
averagePay
[
^self totalPay / self yearsOfService.
] on: ZeroDivide do: [:ex |
^0.
].
%
The first argument to the on:do: method specifies what types of exception the handler should catch. The argument can be a class in the Exception hierarchy, or it can be an ExceptionSet made up of one or more classes in the Exception hierarchy.
The second argument specifies a one-argument ExecBlock that will be invoked when the specified exception is signaled. The one argument is the newly-created instance of the class of the exception that was signaled, and can contain additional information about the exception (including the string that was passed to the signal: method). For example, an instance of the ZeroDivide error can be queried for the dividend (obviously, the divisor is zero). Similarly, an instance of the MessageNotUnderstood error can be queried for the receiver and message (selector and arguments).
When an exception is signaled, GemStone starts at the top of the current process’s stack, searching down the stack for a handler that handles the exception. Each exception handler in the stack is examined to see if it was installed (using the on:do: message) as a handler for the signaled exception’s class. If a handler is found but it does not handle the signaled exception, it is passed over and the search continues down the stack.
A handler for a superclass will handle subclass exceptions. That is, an exception handler for the class Error will be invoked for an exception of its subclass ZeroDivide, and an exception handler for the class Notification will be invoked for an exception of its subclass Warning.
A subclass does not, however, handle a superclass exception. This means that an exception handler for the class MessageNotUnderstood will not be invoked for an exception of its superclass Error.
Example 14.3 contains six blocks, three protected blocks and three handler blocks. Each of the three on:do: messages creates a new stack frame that has an associated handler block.
method: Employee
doStuff
| a b c |
a := [
self doStuffA.
b := [
self doStuffB.
c := [
self doStuffC.
self doStuffD.
] on: ZeroDivide do: [:zdEx |
self handleZeroDivide: zdEx.
^self.
].
self doStuffE.
] on: Warning do: [:wEx |
self handleWarning: wEx.
wEx resume: #ok.
].
self doStuffF.
#good.
] on: Error do: [:erEx |
self handleError: erEx.
erEx return: #bad.
].
%
As shown in Figure 14.3, the handler for Error is installed first, and catches any Error or subclass exception signaled during the block that begins with self doStuffA.
The handler for Warning is installed next, and catches any Warning or subclass exception signaled during the block that begins with self doStuffB.
If a ZeroDivide error is signaled during doStuffB, it is handled by the Error handler, not by the ZeroDivide handler (which is not yet installed).
The handler for ZeroDivide is installed last, and catches any ZeroDivide error or subclass exception signaled during the block that begins with self doStuffC.
If a MessageNotUnderstood error were signaled during doStuffC, it would not be handled by either the ZeroDivide or Warning handler, even though they were installed more recently. Those handlers are not of the proper class; MessageNotUnderstood does not inherit from ZeroDivide or Warning. Instead, a MessageNotUnderstood error would be handled by the Error handler associated with the block that begins with self doStuffA.
Once control is passed by sending value: to the handler block with the exception instance as an argument, the handler block can attempt to address the situation.
Keep in mind that a dynamic handler is just an ExecBlock that is defined in a method and passed as an argument during a message send (like a block sent with a select: message). As such, the dynamic handler has access to the method context in which it is defined, including method temporaries and block variables in its scope, as well as the object in which the method is defined (including instance variables). The handler may, of course, send messages to any object to which it has access.
In particular, the dynamic handler may return from the method containing the dynamic handler. In Example 14.3, the ZeroDivide handler returns self. If a ZeroDivide exception were signaled during doStuffC, then the doStuff method would return and other messages would never be sent (doStuffD, doStuffE, and doStuffF).
In addition to an explicit return from the containing method, a dynamic handler can send the following messages to the exception instance to cause other changes in the flow of control. Sending one of these messages is similar to a method return in that there is no return from these messages (except for outer, which might return).
resume: anObject
Causes anObject to be returned as the result of the signal: message that triggered the exception. Sending resume: to a non-resumable exception is an error.
In Example 14.3, the Warning handler returns #ok as the result of the signal: message.
resume
Causes nil to be returned as the result of the signal: message. Sending resume to a non-resumable exception is an error.
return: anObject
Causes anObject to be returned as the result of the on:do: message to the protected block. In Example 14.3, the Error handler returns #bad to the local variable ‘a’ as the result of the on:do: message. If no Error occurred during the protected block, then the on:do: method would return #good as the result of evaluating the protected block.
return
Causes nil to be returned as a result of the on:do: message.
retry
Unwinds the stack and re-evaluates the protected block (by sending the on:do: message again).
retryUsing: aBlock
Unwinds the stack and evaluates the replacement block as the protected block, sending it the on:do: message.
pass
Exits the current handler and searches for the next handler. In Example 14.3, if the ZeroDivide handler sends pass to the ZeroDivide exception instance, control passes to the Error handler as if the ZeroDivide handler didn’t exist (except that any side effects of its operation up to the pass message are preserved).
outer
Similar to pass, except that if the outer handler sends resume: or resume to the exception instance, control returns to the inner handler from the outer message.
resignalAs: replacementException
Sending this message causes GemStone Smalltalk to start searching for an exception handler for replacementException at the top of the stack as if the original signal: message had been sent to replacementException instead of the receiver.
NOTE
If none of the above messages are sent to alter the flow of control, the value of the last expression in the block will be returned as the result of the on:do: message. (For clarity, you could make this behavior explicit by using the return: message.)
As described above, a dynamic (stack-based) handler protects a particular block of code that exists in the same method as the handler. This is appropriate when you only want to handle a particular exception during execution of the protected code. When the protected block finishes executing, the handler is no longer in effect.
There are, however, other exceptions that could happen at any time for reasons entirely unrelated to your code — for example, being notified that the disk is full (RepositoryError) or that another Gem is sending you a signal (InterSessionSignal). For such exceptions, you can establish a default (or static) handler.
Since ANSI does not provide a direct API for adding and removing default handlers at runtime, GemStone provides the following methods to deal with default handlers in the context of the ANSI framework.
Exception class >> addDefaultHandler: aOneArgumentBlock
Returns a GsExceptionHandler that understands the message remove and adds the new handler to the beginning of the defaultHandlers list. After aOneArgumentBlock (equivalent to the second argument to on:do:) is invoked, the argument (an instance of Exception or one of its subclasses) responds appropriately to pass and outer seamlessly between stack-based and default handlers.
AbstractException class >> defaultHandlers
Returns a SequenceableCollection (or subclass) of GsExceptionHandler instances that will catch instances of the receiver (typically, a subclass of AbstractException). The result does not include any legacy static handlers (as discussed here). This collection may be empty and typically is a subset of the installed default (static) handlers.
GsExceptionHandler >> remove
Since a default handler is not tied to a specific block of code, once installed it remains in effect until explicitly removed (or until the session logs out). This method removes (and returns) the default handler if it is found. If it is not found, returns nil.
The third line of defense for an exception (after dynamic and default handlers) occurs when the virtual machine sends the message defaultAction to the signaled exception. Because defaultAction is implemented in AbstractException, every exception will eventually be handled. The ultimate default action (in AbstractException) is to stop the GemStone Smalltalk interpreter and pass the exception back to the client (to be handled by Topaz, GemBuilder, or another application).
Exception subclasses can override this method to provide alternate behavior. For example, the default action for Notification is to ignore the notification and return nil from the signal: message. For Deprecated, the default action is to log information; for MessageNotUnderstood, the default action is to retry the original action.
To define a default handler for a new exception, add a defaultAction method to your new exception class.
ANSI exception handling, as described previously, is the primary mechanism for dealing with errors in your programs. The legacy handler protocol is deprecated, and all exceptions are now raised as ANSI exceptions. While we strongly encourage the use of ANSI protocol, legacy protocol may be used to raise and handle ANSI exceptions.
In ANSI, a dynamic (stack-based) exception handler is associated with an ExecBlock. By contrast, a dynamic legacy exception handler is associated with a method being executed. These exception handlers live and die with their associated method contexts—when the method returns, control is passed to the next method and the exception handler is gone.
Each exception handler is associated with one method context, but each method context can have a stack of associated exception handlers. The relationship is diagrammed in Figure 14.4.
To define a legacy dynamic (stack-based) handler for an exception, use the class method Exception category:number:do:.
If your exception handler does not specify an error number (an error number of nil), then it receives control in the event of any exception.
The exception handler in Example 14.4 catches the GemStone exception ZeroDivide and returns either PlusInfinity or MinusInfinity, depending on the sign of the dividend.
A default (static) exception handler is a final line of defense—if you define one, it will take control in the event of any error for which no other handler has been defined. A static exception handler executes without changing in any way the stack, or the return value of the method that called it. Static exception handlers are therefore useful for handling errors that appear at unpredictable times, such as the errors listed in Table 14.1. You can use a static exception handler as you would an interrupt handler, coding it to change the value of some global variable, perhaps, so that you can determine that an error did, in fact, occur.
To define a default (static) exception handler, use the Exception class method installStaticException:category:number:.
The following exception handler, for example, handles the error #abortErrLostOtRoot:
The errors in Table 14.1 are sometimes called event exceptions. Although they are not true errors, their implementation is based on the GemStone error mechanism. For examples that use these event exceptions, also called signals, see Chapter 13, “Signals and Notifiers”.
In Table 14.1, the legacy error symbol (and number) is listed along with the corresponding current exception class.
NOTE
The array LegacyErrNumMap (in Globals) describes the mapping of legacy (pre-3.0) error numbers to ANSI exception classes (as described in Chapter 14, “Handling Exceptions”).
Exception handlers with no explicit return operate like interrupt handlers—they return control directly to the method from which the exception was raised. You must write all default (static) exception handlers this way, because the stack usually changes by the time they catch an error. Dynamic (stack-based) exception handlers can also be written to behave that way, like the one in Example 14.4. See Figure 14.5.
Sometimes, however, this is not useful behavior—the application may simply have to raise the same error again. In dynamic (stack-based) exception handlers, it can be useful instead to return control to the method that defined the handler.
You can accomplish this by defining an explicit return (using the return character ^) in the block that is executed when the exception is raised. For example, the method in Example 14.6 redefines how the GemStone exception #ZeroDivide is to be handled.
| a b c |
a := 0.
Exception
category: GemStoneError
number: 2026
do: [:ex :cat :num :args |
"Return from this method with a String"
^'zero divide'
].
"When a is zero, the error will be caught and the method will
return without assigning any value to b or c"
b := -10 / a.
c := b * 3.
c
Under certain circumstances, your exception handler can choose to pass control to a previously defined exception handler, one that is below the present exception handler on the stack. To do so, your exception handler can send the message resignal:number:args:.
You can define an exception so that it removes itself after it has been raised, using the Exception instance method remove. In conjunction with the resignal: mechanism described in the previous section, remove allows you to set up your application so that successive occurrences of the same error (or category of errors) are handled by successively older exception handlers that are associated with the same context.
For example, suppose we execute the following code:
| x y |
Exception
category: GemStoneError
number: 2026
do: [:ex :cat :num :args | ex remove. 'first result'].
Exception
category: GemStoneError
number: 2026
do: [:ex :cat :num :args | ex remove. 'second result'].
x := 1 / 0. "handled by the second (most recent) handler"
y := 2 / 0. "handled by the first handler; the second was removed"
Array with: x with: y.
%
anArray( 'second result', 'first result')
The first occurrence of the error executes the most recent exception defined. The exception then removes itself, so that the next occurrence of the same error executes the exception handler stacked previously within the same method context. This exception handler returns an array of two strings, as shown here.
If you define an exception handler broadly to handle many different errors, and you make a programming mistake in your exception handler, the exception handler may then raise an error that calls itself repeatedly. Such infinitely recursive error handling eventually reaches the stack limit. The resulting stack overflow error is received by whichever interface you are using.
If you receive such an error, check your exception handler carefully to determine whether it includes errors that are causing the problem.
Legacy methods for raising exceptions can be used, but raise ANSI exceptions.
To raise an exception, use the class method System signal:args:
signalDictionary:.
To raise the generic exception defined for you in ErrorSymbols as #genericError, use the class method System genericSignal:text:args:, or one of its variants.
Other variants of this message are System genericSignal:text:arg: for errors having only one argument, or System genericSignal:text: for errors having no arguments.
The ANSI and legacy frameworks should work together so that signaling an ANSI exception is caught by a legacy exception handler. Example 14.8 shows a sample use of a legacy handler to catch signaled ANSI exceptions.
method: Employee
legacyMethod
self doA.
"Install a legacy handler"
Exception
category: nil
number: nil
do: [:ex :cat :num :args |
self handlerCode.
self shouldReturn ifTrue: [
^self returnValue.
].
self continueValue.
].
self doB.
"Signal an ANSI error"
instVar1 := Error signal: 'something bad happened!'.
self doC.
^instVar2.
%
When this method is invoked, it calls doA before installing the exception handler. After the exception handler is installed, the method calls doB. If any exception is signaled during the execution of doB, the handler is invoked.
Next, an explicit error is invoked, using the ANSI protocol. This signaled ANSI exception is caught by the legacy exception handler installed earlier in the method. After evaluating the handlerCode, the handler decides whether to return from the method or continue. If it returns, the result of returnValue is returned. If it continues, the result of continueValue is stored in instVar1, and the method proceeds with doC and finally returns instVar2.