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.
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
Since subclasses of
ArithmeticValue divisionByZeroSignal.
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:
because signals and errors respond to the "," (comma) message,
a shorter and more readable version is:
|s|
s := SignalSet
with:(Object messageNotUnderstoodSignal)
with:(ArithmeticValue arithmeticSignal).
...
(Notice:
(Object messageNotUnderstoodSignal , ArithmeticValue arithmeticSignal)
handle:[:ex | ....
] do:[
...
]
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).
The handler can decide how to react to the signal - it may:
#handle:do:
method.
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:
Note if you are reading this using the ST/X doc reader:
|result arg divisor|
arg := 5.
divisor := 0.
"bad division here ..."
result := arg / divisor.
Transcript showCR:('the result is ' , result printString).
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:
The interesting thing in the above code is the
|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).
'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.
If the handler has no explicit
|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).
#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:
To define a handler for all arithmetic errors, we can
use the parent of all arithmetic error signals:
|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).
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:
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:'an error occurred.'.
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 creator printString , ')'.
ex return.
] do:[
"now divide ..."
result := arg / divisor
].
Transcript showCR:('the result is ' , result printString).
|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).
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:
Notice: typically, handler blocks are not nested explicit as in the above
example; instead, the nesting is burried in some called methods.
|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).
]
].
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:
is almost equivalent to the following 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:'.
].
(remember that Error
is the parent
of all other errors;
thus handling this root also handles any other exception too):
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.
|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:'.
].
If the NoHandlerError
is not handled, a debugger is entered normally
(but see below for what is really going on).
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:
|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.
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 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:
For a typical application of these per-process handlers, see how the
|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
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
.
#return
, #proceed
or #reject
) behaves like a #return
.
#handle:do:
handler
#handle:do:
handler(s)
#handle:do:
handlers
GenericException
-class
GenericException
) is always present,
because the system defines it during early initialization.
It will unconditionally enter the debugger.
catch:[...]
ignoreIn:[...]
Object » warnSignal
and Object informationSignal
respectively.
handle:do:
protocol (actually, they are).
From the above order of evaluation, it should now be clear, that errors
can be completely ignored by either:
or (for the current process individually):
GenericException emergencyHandler:[:ex | ]
If you want to try it, don't forget to cleanup afterwards by either:
Processor activeProcess emergencySignalHandler:[:ex |]
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.
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:
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
|sigUsr mySignal|
sigUsr := OperatingSystem sigUSR1.
mySignal := Signal new.
OperatingSystem
operatingSystemSignal:sigUsr
install:mySignal.
#handle:do:
):
Before the signal is finally delivered, the OS signal has to be enabled.
This is done by:
mySignal handlerBlock:[:ex |
"do whatever has to be done ..."
]
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
OperatingSystem enableSignal:sigUsr
"^ 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):
SIGINTR
(i.e. Ctrl-C),
SIGALARM
(for timer)
SIGFPE
(floating point exception).
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:
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).
...
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"
...
]
]
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:
if the
SomeClass
...
classVariableNames:'WarnBoxQuery'
...
initialize
...
WarnBoxQuery := QuerySignal new.
WarnBoxQuery defaultValue:true.
...
warnBoxQuery
^ WarnBoxQuery
actionMethod
...
someErrorOccured ifTrue:[
WarnBoxQuery raise ifTrue:[
self warn:'some error occured'
]
].
...
#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:
The querySignals
...
SomeClass warnBoxQuery
answer:true
do:[
...
invoke actionMethod
...
]
#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:
or try (as a proof):
Object warningSignal
answer:false
do:[
...
something which may show
a warnBox otherwise
...
]
Object warningSignal
answer:false
do:[
self warn:'no no no'
]
"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>