[prev] [up] [next]

Exception Handling and Signals

Contents

NOTICE

This document describes the somewhat obsolete (but still supported) Signal mechanism, which was mostly being replaced by a class based exception implementation which is described in the Ansi standard.
However, both the protocol and the mechanism are still the same, although the parent-relationship is now based upon class inheritance, instead of the signals parent. The old Signal based framework is (and will) still be useful for dynamically generated, local signals which are to be visible inside a single method or class only.
Therefore, the description below is still valid and useful, even if implementation details might be different.

Introduction

Whenever some error occurs in Smalltalk/X (such as a division by zero), the system does not simply crash or terminate, but instead notifies the running program via an exception mechanism. There are many reasons, why a sophisticated error handling mechanism is needed in Smalltalk, the most obvious is of course, that you are working in the system and don't want to get kicked out due to some minor error. Another one is that many errors can be handled by the program and appropriate repair or at least user information is desired and possible. Especially, when evaluating doIt expressions or while testing new methods, errors can happen quite easily.

To do this, all errors have associated with them a so called Exception or Signal object (not to confuse with Unix-Signals), which gets notified of the error. In Smalltalk terms, this is called "raising an exception" (or sometimes, for historical reasons, "raising a signal").

Your program can provide a handler for such an exception, which can decide what to do with the error. If no handler exists, a reasonable default action is taken - typically, a debugger is entered, but there are also signals which are ignored by default.

Notice (if you read this in the ST/X help view):
some of the examples below do not show the expected behavior IF executed in the help viewer. The reason is that the help viewer itself catches any exception that might occur in an example and aborts the example (after showing a warning). You should copy-paste those examples into a workspace and execute them there.

Signals, Parents and SignalSets

Usually, signals which can be raised by some class are kept in the class as class variables and created during early initialization of the system. Most classes provide access to those signals via getter methods on the class side (typically, those methods are categorized as "Signal constants", so you may easily find them with browser's "spawn category" function).

For example, the signal raised upon division-by-zero errors is kept in the class ArithmeticValue as class variable DivisionByZeroSignal. The method that provides access to this signal is

      ArithmeticValue divisionByZeroSignal.
Since subclasses of ArithmeticValue share its class variables and inherit its methods, all numeric classes will of course also respond to this message and return the signal object (i.e. Float divisionByZeroSignal will also work and return the same object).

If the new class based exception mechanism is used, the corresponding getter method will return the exception class instead. So for the outside user, it is transparent whether the class uses signals or class based exceptions to signal an exception.

Every signal (except for the top, GenericException) has a parent. Whenever a handler is specified for a parent, all child signals are also handled by this handler. There are some signals which are never raised directly, but instead exist as parent signals of others for the purpose of handling them all at once. An example of such a signal is ArithmeticValue » arithmeticSignal, which is a parent of all other numeric error signals.

Sometimes, you may want to handle a group of unrelated signals with a common handler (i.e. signals which do not have a common parent). For example, in a desk-calculator you may decide to handle any numeric error and the messageNotUnderstood signals with a common handler. To support this, Smalltalk/X provides groups of signals, so called SignalSet and ExceptionhandlerSet.
A signalSet may contain a number of signals and also responds to the #handle:do: and #catch: messages in much the same way as signals do (more details below).
A signalSet to catch the above errors could be created with:

    |s|

    s := SignalSet
	    with:(Object messageNotUnderstoodSignal)
	    with:(ArithmeticValue arithmeticSignal).
    ...
because signals and errors respond to the "," (comma) message, a shorter and more readable version is:
    (Object messageNotUnderstoodSignal , ArithmeticValue arithmeticSignal)
	handle:[:ex | ....
	] do:[
	    ...
	]
(Notice: SignalSet is inheriting from Collection; therefore the #with:with: is understood by the SignalSet class - but you will need individual #add: messages if a set with more than 8 signals is to be created).

Signal Handlers

Signals are handled by providing a signal handler (-block) for the duration of some computation. Whenever the handled signal is raised during the computation, the handler will be evaluated. The handler gets the reason (i.e. the signal) and location (i.e. the context) of the exception as argument; wrapped into a so-called exception object. An optional additional parameter and an error string are also available in the exception object.

The handler can decide how to react to the signal - it may:

Many applications must perform operations "save" from entering the debugger. Such a "save" computation is performed by passing both the handler-block and the block which does the actual computation to the signals #handle:do: method.
The following shall make this somewhat clearer:
    aSignal
	handle:[:ex |
	    "this is the handler ...
	     ex is the exception object - see more below."
	]
	do:[
	    "this is the computation"
	]
In the above, whenever aSignal is raised during evaluation of the second block (the one after 'do:'), the handler block (the one after 'handle:') is evaluated. The handler is a block with one argument (ex) in which the exception object (containing the exception info) will be passed.

A concrete example:
In a desk calculator, you want to catch division-by-zero errors (end-users should normally not be confronted with the internals presented by debuggers).
Without exception handling, the following code will raise a divisionByZero exception and bring you into the debugger:

    |result arg divisor|

    arg := 5.
    divisor := 0.

    "bad division here ..."
    result := arg / divisor.

    Transcript showCR:('the result is ' , result printString).
Note if you are reading this using the ST/X doc reader:
the reader catches any error signals itself - here, no debugger is opened; instead, only a warnbox is shown.

With exception handling, the exception is handled locally, and a message is sent to the transcript instead:

    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue divisionByZeroSignal
	handle:[:ex |
	    Transcript showCR:'an error occurred.'.
	    ex return
	]
	do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).
The interesting thing in the above code is the 'ex return'. This is the handler's action (in this case returning from the faulty computation, and continuing after the #handle:do: message).

The above handler simply returned from the computation block and therefore left the result undefined. You can also take the value of the #handle:do: and thereby use either the doBlocks regular return value, or a value from the handler:

    |result arg divisor|

    arg := 5.
    divisor := 0.

    result :=
	(ArithmeticValue divisionByZeroSignal
	    handle:[:ex |
		Transcript showCR:'an error occurred.'.
		ex returnWith:999999999999
	    ]
	    do:[
		"now divide ..."
		arg / divisor
	    ]).
    Transcript showCR:('the result is ' , result printString).

Let's try another one, in which the handler restarts the computation (not without changing the divisor first ...):

    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue divisionByZeroSignal
	handle:[:ex |
	    Transcript showCR:'an error occurred.'.
	    Transcript showCR:'retry with 1 as divisor.'.
	    divisor := 1.
	    ex restart
	]
	do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).

Of course, with our calculator example, restarting with a changed divisor may not be a good idea. However, in many real-world applications, a handler could fix things and restart the operation. This is especially true for file not found and communication breakdown type of problems.

The handler can also decide to not handle the signal, and let it be handled by someone else. This is useful when handlers are nested, and another #handle:do: context is already calling this one or if a handler can do some partial repair or cleanup work, but still wants other error handlers to be executed. Finally, it is useful if a handler can sometimes repair things and continue the computation, but should report the error to higher levels if it was not able to fix things.

Since there is no other handler in the example below, you will end up in the debugger after the handler has sent its message to the Transcript.

    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue divisionByZeroSignal
	handle:[:ex |
	    Transcript showCR:'an error occurred.'.
	    Transcript showCR:'I give it to the next handler...'.
	    ex reject
	]
	do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).
If the handler has no explicit #return, #proceed or #reject, but instead simply falls through to the end of the block, it will behave as if a #returnWith:val was present, where val is the handler block's evaluated value (i.e. the value of its last expression).

If your application is a graphical one, you might prefer to show some warn-box in the handler (your end-user application may not have a Transcript). This is of course straight forward:

    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue divisionByZeroSignal
	handle:[:ex |
	    self warn:'an error occurred.'.
	    ex return.
	]
	do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).
To define a handler for all arithmetic errors, we can use the parent of all arithmetic error signals: ArithmeticError.
This signal is never raised itself, however, since a handler for a parent signal will also handle child signals, defining a handler for the ArithmeticError signal will also handle division-by-zero and all other arithmetic signals:
    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue arithmeticSignal
	handle:[:ex |
	    self warn:'an error occurred.'.
	    ex return.
	]
	do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).
You may want to tell the user which signal was actually responsible for the raise; you can ask the exception object for the creating signal, as in:
    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue arithmeticSignal
	handle:[:ex |
	    self warn:'Error occurred (' , ex creator printString , ')'.
	    ex return.
	] do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).
or you can even get a textual description, ready to be shown:
    |result arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue arithmeticSignal
	handle:[:ex |
	    self warn:'Error occurred: ' , ex description.
	    ex return.
	] do:[
	    "now divide ..."
	    result := arg / divisor
	].
    Transcript showCR:('the result is ' , result printString).

Nested Handlers

Handlers can be nested - for example, some methods can decide locally on how to handle an exception and continue gracefully. These may define a local signal handler and repair things. Any existing outer handler may or may not be notified (if the local handler rejects, the outer handler will see the exception; otherwise, the outer handler will not see anything).

An nice application for nested handlers can be found within the current smalltalk system:
whenever the abort-button is pressed in the debugger (or the CMD-Y-key is pressed), an AbortSignal is raised, to return to some save place up in the calling hierarchy. The main event dispatcher handles this signal by simply continuing in its event loop.
A workspace on the other hand, handles errors locally, and may output an error message - in this case, the event-dispatcher will not see any error at all.

Example:

    |result1 result2 arg divisor|

    arg := 5.
    divisor := 0.

    ArithmeticValue anyArithmeticSignal
    handle:[:ex |
	"(outer) handler for any arithmetic exception"

	Transcript showCR:'an arithmetic error occurred'.
	ex return.
    ]
    do:[
	"the computation"

	ArithmeticValue divisionByZeroSignal
	    handle:[:ex |
		"(inner) handler for division by zero"

		Transcript showCR:'division by zero ignored'.
		ex proceedWith:0
	    ]
	    do:[
		"now divide ..."
		result1 := arg / divisor.
		Transcript showCR:('result of division: ' , result1 printString).
		result2 := arg arcSin.
		"not reached, since outer handler returns"
		Transcript showCR:('result of arcSin: ' , result2 printString).
	    ]
    ].
Notice: typically, handler blocks are not nested explicit as in the above example; instead, the nesting is burried in some called methods.

Unhandled Exceptions

Whenever an exception is not handled (i.e. either no handler was present, or all of them rejected), the runtime system will raise another exception, called the NoHandlerError (used to be NoHandlerSignal in the past). This exception wraps information on the unhandled exception, and can of course be handled just like any other exception or signal. The default action for it is to open a debugger - which is why you end in the debugger for all other unhandled signals.

Thus, handling the NoHandlerError is almost equivalent to handling all other signals.
Example:

    |result arg divisor|

    arg := 5.
    divisor := 0.

    HandlerError
	handle:[:ex |
	    Transcript showCR:'some error occurred.'.
	    ex proceed
	]
	do:[
	    "divide by zero ..."
	    result := arg / divisor.
	    Transcript showCR:'after division'.

	    "send some bad messages ..."
	    Array new:-1.
	    Transcript showCR:'after bad new'.

	    1 at:5.
	    Transcript showCR:'after bad at:'.
	].
is almost equivalent to the following example:
(remember that Error is the parent of all other errors; thus handling this root also handles any other exception too):
    |result arg divisor|

    arg := 5.
    divisor := 0.

    Error
	handle:[:ex |
	    Transcript showCR:'some error occurred.'.
	    ex proceed
	]
	do:[
	    "divide by zero ..."
	    result := arg / divisor.
	    Transcript showCR:'after division'.

	    "send some bad messages ..."
	    Array new:-1.
	    Transcript showCR:'after bad new'.

	    1 at:5.
	    Transcript showCR:'after bad at:'.
	].
The difference lies in the exception-object passed as argument to the handler block. In the first example, the handler will find the NoHandler-context in the exception as location of the raise. In the latter example, the exception will contain the place where the original raise occured, which makes any repair work easier for the handler. Thus, in the first example above, the handler is invoked with somewhat more indirect information.

If the NoHandlerError is not handled, a debugger is entered normally (but see below for what is really going on).

Handler Blocks

Notice: this paragraph applies to the old Signal mechanism and is not valid for class based exceptions. Unless you have to deal with backward compatibility issues, you should skip and forget this paragraph.

Instead of a #handle:do: -handler-context, you may also assign a handler block statically to a signal. This block is called an emergency handler and is only evaluated if no other handler was found, or all handlers rejected.

Warning:

Static handler blocks are associated to the signal itself. They are valid for all processes in any evaluation context. They should be used in special situations only.
Static handlers make sense in the following situations: Signal handlerBlock example:
    |result arg divisor|

    ArithmeticValue divisionByZeroSignal
	handlerBlock:[:ex |
	    Transcript showCR:'division error occured - return 0 instead.'.
	    ex proceedWith:0
	].

    arg := 5.
    divisor := 0.
    result := arg / divisor.

    "we have to cleanup - otherwise, this error will never
     lead to the debugger again ..."

    ArithmeticValue divisionByZeroSignal
	handlerBlock:nil
To demonstrate the global effect of the above, first evaluate:
    ArithmeticValue divisionByZeroSignal
	handlerBlock:[:ex |
	    Transcript showCR:'division error occured - return 0 instead.'.
	    ex proceedWith:0
	].
then open any new workspace, and evaluate:
    |divisor|

    divisor := 0.
    5 // divisor
there. Don't forget to cleanup things after your experiments, with:
    ArithmeticValue divisionByZeroSignal handlerBlock:nil
Notice:
in this demostration, you cannot directly enter '5 // 0', since the compiler checks for zero-divisors BEFORE doing the actual send (when doing constant-folding). Thus the signal will not really be sent when using a constant as divisor. The compiler is (currently) not smart enough to track the values in variables - therefore putting the zero into some variable helps.
There is one limitation in static handler-blocks: no #return or #restart actions are allowed (i.e. these actions will lead to another error) in the handler.
The reason is simple: return/restart try to return from/restart the evaluation-block. Since there is no #handle:do: context for static handlers, no such evaluation-block is available to return from. Static handlers should either proceed, reject or terminate the current process. (Usually after showing some warnBox or querying the user).

Per Process Emergency Handler

You can define a per-process emergency handler block, which will be evaluated if the NoHandlerError is unhandled while a particular process (aka thread) is running. Each process can be assigned its own specific handlerblock.

Per-process handlers are useful, if you want to make certain that a process cleans up and terminates gracefully, or if you want to catch any error within the process AND you do not want to (or cannot) add a #handle:do: into the start method of the process's code. One such situation would be, if you do not have the source code of a library which raises errors but does not (for whatever reason) contain appropriate error handling code.
Example:

    |myProcess|

    myProcess := [
	Transcript showCR:'waiting for a while ...'.
	(Delay forSeconds:5) wait.
	Transcript showCR:'doing something bad ...'.
	1 at:5 put:nil.
	"this is not reached"
	Transcript showCR:'after the bad computation'.
    ] newProcess.

    "set the handler"
    myProcess emergencySignalHandler:[:ex |
	self warn:('process terminated due to error:' , ex errorString).
	Processor activeProcess terminate.
    ].

    "let it run"
    myProcess resume
For a typical application of these per-process handlers, see how the Launcher sets up an emergency handler in its realize method, to show a warnbox and optionally abort whatever the user action was. Like static handlers, per-process handlers also do not support #return and #restart.

Order of Handler Invocation

Handlers are invoked in the following order, until some handler either returns or proceeds the exception (i.e. followup-handlers are invoked while handlers reject). Falling through a handler block (without any explicit #return, #proceed or #reject) behaves like a #return.
  1. enclosing #handle:do: handler

  2. next enclosing #handle:do: handler(s)

  3. per-process handler-block for this exception, if defined

  4. NoHandlerError #handle:do: handlers

  5. per-process NohanderError emergency-handler-block

  6. emergencyHandler defined in GenericException-class
The last handler-block (in GenericException) is always present, because the system defines it during early initialization. It will unconditionally enter the debugger.

Simple Handlers to Catch & Ignore Exceptions

Because the task of ignoring an exception within some computation is a pretty common one, the signal protocol includes two messages for a more convenient signal handling: Note that these two messages do not provide any new functionality - they can easily be implemented using the handle:do: protocol (actually, they are).

How to Completely Ignore Errors

This is a frequently asked question: "how can I make certain that no debugger is entered in my app".

From the above order of evaluation, it should now be clear, that errors can be completely ignored by either:

    GenericException emergencyHandler:[:ex | ]
or (for the current process individually):
    Processor activeProcess emergencySignalHandler:[:ex |]
If you want to try it, don't forget to cleanup afterwards by either:
    Processor activeProcess emergencySignalHandler:nil

The GenericException class provides a bunch of useful handlers in its "useful handlers" class protocol - have a look at those; there may be a useful one there for your application.

Unix-Signals vs. Smalltalk-Signals

Technically, Unix signals have nothing at all to do with Smalltalk signals. However, it is possible to arrange for a Smalltalk signal to be raised whenever a Unix signal arrives. In other words: "to connect a Unix signal to a Smalltalk signal".
This is done by:
    OperatingSystem
	operatingSystemSignal:aNumber
	install:aSignal
Notice, that OS signal numbers are not required to be portable across different Unix versions or OperatingSystems (although they are so for the most common, standard signals). Thus, you should not depend on SIGUSR1 being signal number 10, for example. The OperatingSystem class offers methods to get the various numbers.

Thus, your code should look like:

    |sigUsr mySignal|

    sigUsr := OperatingSystem sigUSR1.
    mySignal := Signal new.

    OperatingSystem
	operatingSystemSignal:sigUsr
	install:mySignal.
then, since Unix signals may occur at any time (especially: in another process), you should assign a static handler block to the signal (instead of a #handle:do:):
    mySignal handlerBlock:[:ex |
	"do whatever has to be done ..."
    ]
Before the signal is finally delivered, the OS signal has to be enabled. This is done by:
    OperatingSystem enableSignal:sigUsr
One thing has to be kept in mind with these handlerblocks: since they may be evaluated in whatever Smalltalk process is running at signal time, these blocks should not (cannot) return or otherwise modify the context chain. I.e. a "^ something" is not allowed from these blocks.

Not all Unix signals can be assigned a smalltalk signal: certain signals will always be handled by smalltalk itself. These are (among others):

these are handled slightly different, but can also be cought by defining an object which is notified whenever these occur. (see ``timers. delays and interrupts'').

Private Signals and Exception Classes

It should be clear, that the exception mechanism is not limited to the above mentioned system error conditions. Instead, it can be used, whenever information about unexpected situations has to be forwarded to higher levels in the calling hierarchy (also called upCasts) or information is queried from someone higher in the calling hierarchy (could we call this an upQuery ?).

You can create your own private signals in your classes #initialize method, or even create them temporary (and/or anonymous) for use within a single method. Alternatively, you can define public or private classes which inherit from Exception, Error or any other existing exception class.

Although not obvious, the exception mechanism can also be used for a two-way communication of some deeply nested code with any outer handler (i.e. in which the handler never returns, but always proceeds execution with a value). Since there are many uses for this kind of relaxed exception, ST/X provides two special Query and Notification classes, which offer more convenient (i.e. readable) protocol, and whose default handler behavior has been redefined to proceed. Please read the section below for more details.

Concrete examples are found in the handling of a changed instance layout in the binary storage implementation, in the implementation of non local (fluid) variables and especially in how the compiler reports compilation warnings to its caller.

In the binary storage example, a signal is used to ask for a converter which is able to migrate outdated objects. In the fluid variable example, it is used to ask for the latest value binding of a variable.
In both, this is an upQuery call to whichever handler is in charge of deciding things.

Finally, private signals can be used to implement the traditional catch&throw mechanism, as in:

    ...
    sig1 := Signal newSignal.
    sig2 := Signal newSignal.
    ...
    sig1 handle:[:ex |
	...
	catch code for sig1
	...
    ] do:[
	sig 2 handle:[:ex |
	    ...
	    catch code for sig2
	    ...
	] do:[
	    ...
	    someCondition ifTrue:[sig1 raise].   "throw sig1"
	    ...
	    someCondition ifTrue:[sig2 raise].   "throw sig2"
	    ...
	]
    ]
of course, if the throw is not located in the same method, you have to make the signal be accessible in the called code (via class or instance variables, via a getter method or by defining an exception class).

Queries and Notifications

In addition to regular exceptions (which raise a NoHandlerError and thus enter the debugger if unhandled), ST/X provides two other kinds of exceptions, called Query and Notification. Like other signals or exceptions, these can be raised and an exception handler will catch them if. However, in contrast, these do not lead into the debugger if unhandled; instead, the execution continues after the raise, and the query is answered with some default value.
If a handler is present, it is expected to proceed with some value.

Queries are used in situations, where a nested method requires some information which is optionally provided by the caller, AND the programmer does not want to pass that information through all the intermediate method invocations via arguments or static data.
For example, a deeply nested method may encounter a problem when performing some action. It may want to display this trouble by opening a warnbox. This is ok, if the operation was originally invoked by the user, but annoying, if some other program wants to use this functionality, but does not want any warnboxes to appear.

Here, an up-query for a notification could be used; if unhandled, the notification default value as returned from the raise leads to a warnbox being shown:
The corresponding code is:

     SomeClass
	 ...
	 classVariableNames:'WarnBoxQuery'
	 ...

	 initialize
	    ...
	    WarnBoxQuery := QuerySignal new.
	    WarnBoxQuery defaultValue:true.
	    ...

	 warnBoxQuery
	    ^ WarnBoxQuery

	 actionMethod
	    ...
	    someErrorOccured ifTrue:[
		WarnBoxQuery raise ifTrue:[
		    self warn:'some error occured'
		]
	    ].
	    ...
if the #actionMethod is simply called for, and an error occurs, the warnbox will be shown.
If some program wants to use the actionMethod without any boxes being shown, the following code will do:
    ...
    SomeClass warnBoxQuery
	answer:true
	do:[
	     ...
	     invoke actionMethod
	     ...
	]
The querySignals #answer:do: method implements a handler which proceeds with the answer-value - it is provided to make querySignal handlers more readable.

Within ST/X, querySignals are used by the compiler to ask for the current nameSpace, by the changeList manager, to ask if a change is to be recorded, and by the #notification / #warn methods, which ask if corresponding dialog boxes are to be shown.

For example, to suppress the opening of the warnBox, the following code will do:

    Object warningSignal
    answer:false
    do:[
	...
	something which may show
	a warnBox otherwise
	...
    ]
or try (as a proof):
    Object warningSignal
    answer:false
    do:[
	self warn:'no no no'
    ]

Further reading

You will find examples in "doc/coding" which show various aspects and code examples of signal handling.

The above mentioned Binding example is found in "goodies/Misc-Bindings.st.

See ``list of signals'' for a table of signals and a description of when they are raised.


Copyright © 1995 Claus Gittinger, all rights reserved

<cg at exept.de>

Doc $Revision: 1.38 $ $Date: 2021/03/13 18:24:51 $