14. Handling Exceptions

Previous chapter

Next chapter

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.

14.1 The Exception Class Hierarchy

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.

Figure 14.1 Exception 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 

14.2 Signaling Exceptions

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.

Example 14.1

method: Employee
age: anInt
(anInt between: 15 and: 65)
	ifFalse: [Error signal: 'Employee age out of range'].
age := anInt.
%
 

14.3 Handling Exceptions

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.

Dynamic (Stack-Based) 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.

Figure 14.2 ExecBlock and Associated Handlers

		
 

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

Example 14.2

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

Selecting a Handler

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.

Example 14.3

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.

Figure 14.3 Selecting a Handler

Newest or innermost call

Handler

 

 

 

doStuffC

ZeroDivide

Direction

of

Search

 

doStuffB

Warning

 

doStuffA

Error

Oldest or outermost call

 

 

 

 

 

Flow of Control

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

Messages That Alter the Flow of Control

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

Default Handlers

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.

Default Actions

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.

14.4 The Legacy Exception Handling Framework

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.

Dynamic (Stack-Based) Exception Handler

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.

Figure 14.4 Method Contexts and Associated Handlers

 

Installing a Dynamic (Stack-Based) Exception Handler

To define a legacy dynamic (stack-based) handler for an exception, use the class method Exception category:number:do:.

  • The argument to the category: keyword is ignored.
  • The argument to the number: keyword is the specific error number you wish to catch, which can be nil (to catch all exceptions).
  • The argument to the do: keyword is a four-argument block you wish to execute when the error is raised.
  • The first argument to the four-argument block is the instance of Exception that was signaled.
  • The second argument to the four-argument block is always GemStoneError.
  • The third argument to the four-argument block is an error number.
  • The fourth argument to the four-argument block is the data passed in when invoking the error.

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.

Example 14.4

| a b c |
a := 0.
Exception
	category: GemStoneError
	number: 2026
	do: [:ex :cat :num :args | 
		"Return a value as a result of the #'/' message"		
		ex dividend * 1.0e0 / 0].
 
"This might give a ZeroDivide error, 
depending on the value of a"
b := -10 / a.	 
c := b * 3.
c
 

NOTE
Keep the handler as simple as possible, because you cannot receive any additional errors while the handler executes. Normally your handler should never terminate the ongoing activity and change to some other activity.

Default (Static) Exception Handlers

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.

Installing a Default (Static) Exception Handler

To define a default (static) exception handler, use the Exception class method installStaticException:category:number:.

  • The argument to the installStaticException: keyword is the block you wish to execute when the error is raised.
  • The argument to the category: keyword is ignored.
  • The argument to the number: keyword is the specific error number you wish to catch.

The following exception handler, for example, handles the error #abortErrLostOtRoot:

Example 14.5

UserGlobals at: #tx3 put:
   ( "Handle lost OT root"
     Exception
       installStaticException: [:ex :cat :num :args |
         System abortTransaction.
       ]
     	 category: nil
     	 number: 3031
    	 subtype: nil
   ).
 

To remove the handler, execute:

self removeExceptionHandler: (UserGlobals at: #tx3).

GemStone Event Exceptions

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

 

Table 14.1 Common GemStone Event Exceptions

Exception class

Legacy symbol (and number)

Description

TransactionBacklog

 

#rtErrSignalAbort (6009)

#rtErrSignalFinishTransaction (6012)

When System inTransaction returns false (running outside a transaction), Stone requested Gem to abort. This error is generated only if you have executed either System enableSignaledAbortError or TransactionBacklog enableSignalling.

When System inTransaction returns true (the session is in transaction), Stone has requested the session to commit, abort, or continue (with continueTransaction) the current transaction. This error is received only if you have executed either System enableSignaledFinishTransactionError or TransactionBacklog enableSignalling.

ObjectsCommittedNotification

 

#rtErrSignalCommit (6008)

An element of the notify set was committed and added to the signaled objects set. This error is received only if you have executed either System enableSignaledObjectsError or ObjectsCommittedNotification enableSignalling

InterSessionSignal

 

#rtErrSignalGemStoneSession (6010)

Your session received a signal from another GemStone session. This error is received only if you have executed either System enableSignaledGemstoneSessionError or InterSessionSignal enableSignalling.
InterSessionSignal arguments:
1. The session ID of the session that sent the signal.
2. An integer representing the signal.
3. A message string.

AlmostOutOfMemory

 

#rtErrSignalAlmostOutOfMemory (6013)

Temporary object memory for the session is almost full. The error is deferred if in user action or index maintenance. This error is enabled by default, but the default handler has no action. After a signal is received, it must be reenabled using System enableAlmostOutOfMemoryError or
AlmostOutOfMemory enable.

RepositoryError

 

#rtErrTranlogDirFull (2339)

All available transaction log directories or partitions are full. This error is received if you are DataCurator or SystemUser, otherwise only if you have executed System enableSignalTranlogsFull.

RepositoryViewLost

 

#abortErrLostOtRoot (3031)

While running outside a transaction, Stone requested Gem to abort. Gem did not respond in the allocated time, and Stone was forced to revoke access to the object table.

Flow of Control

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.

Figure 14.5 Default Flow of Control in Legacy Exception Handlers

 

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.

Example 14.6

| 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
 

Figure 14.6 shows the flow of control in Example 14.6.

Figure 14.6 Dynamic (Stack-Based) Exception Handler with Explicit Return

 

Signaling Other Exception Handlers

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

  • The argument to the resignal: keyword is ignored.
  • The argument to the number: keyword is the specific error number you wish to signal.
  • The argument to the args: keyword is an array of information you wish to pass to the exception handler. This is the array whose elements might be used to build the error message.

Removing Exception Handlers

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:

Example 14.7

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

Recursive Errors

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.

Raising Exceptions

Legacy methods for raising exceptions can be used, but raise ANSI exceptions.

To raise an exception, use the class method System signal:args:
signalDictionary:
.

  • The argument to the signal: keyword is the specific error number you wish to signal.
  • The argument to the args: keyword is an array of information you wish to pass to the exception handler. This is the array whose elements are passed to the handler.
  • The argument to the signalDictionary: keyword is ignored.

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.

  • The argument to the genericSignal: keyword is an object you can define to further distinguish between errors, if you wish. Alternatively, it can be nil.
  • The argument to the text: keyword is a string you can use for an error message. It will appear in GemStone’s error message when this error is raised. It can be nil.
  • The argument to the args: keyword is an array of information you wish to pass to the exception handler, as described above.

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.

ANSI Integration

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.

Example 14.8

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.

 

Previous chapter

Next chapter