Navigation: Programming Cookbook > Exception Handling >
Exceptions vs. Unwinds
Unwind blocks are the blocks passed as arguments to the BlockClosure>>ifCurtailed: and BlockClosure>>#ensure: methods, and are typically intended to perform clean-up operations on behalf of the receiving block. #ensure: blocks are guaranteed to be run, whether the receiving block exits normally or not. #ifCurtailed: blocks are run only when the receiving block exits abnormally, as a result of a ^-return, an exception, or process termination.
Unwind blocks are very useful for maintaining a valid image state (e.g. for unlocking mutual exclusion Semaphores).
When an exception occurs, the Process stack is unwound after the exception is handled, possibly terminating the process if there is no handler. As the process stack is unwound any unwind blocks encountered along the way are evaluated.
To some extent, exception handler blocks (particularly those which catch Error) can be used as a form of unwind block, but there is a distinction which must be understood: Following the evaluation of an exception handler block, if that block does not perform some explicit handler response, then the unwinding of the stack is terminated at that point, and execution continues immediately after the handler block. In contrast, unwind blocks only momentarily interfere with the progress of the stack unwinding to the return destination. Additionally, exception handler blocks are not evaluated as a result of ^-returns.
For example, consider a more guarded version of Object>>printString, designed to suppress errors occurring in #printOn:
"Answer a String whose characters are a description of the receiver as a developer
would want to see it."
| stream |
stream := WriteStream on: (String new: 10).
"Catch attempts to print an invalid object which might otherwise
throw an exception"
[self printOn: stream] ifCurtailed: [
nextPutAll: 'an invalid ';
display: self class].
"...but not to here"
Although this method would print the 'an invalid XXX' message should an error occur in the #printOn: method, it would not prevent a walkback. #ifCurtailed: is an unwind handler which guarantees that the argument block (and in this case it must be a block for ANSI compatibility, though the Dolphin implementation will work for other niladic valuables too) will be evaluated, if the receiving block (and it must be a block) should attempt to exit directly from the method. It does not prevent the further propagation of the error. There are three circumstances in which this might happen:
|1.||The receiver block itself includes a ^-return (i.e. return from home method context).|
|2.||A block is evaluated inside the context of the receiver block which itself performs a ^-return from its own home method context, where that home method context encloses the receiver block (i.e. it is further down the stack).|
|3.||An exception is raised.|
All of these cases boil down to some attempt to perform a "far" return that returns to some method an arbitrary depth back down the stack. In C++ it is only possible to do this with exceptions (or setjmp()/longjmp()). In Dolphin exceptions are actually implemented by making use of the ability of blocks to do "far" returns - i.e. case 3 is actually a clever use of case 2.
The other important thing to know about #ifCurtailed:, is that it does not prevent the "far" return from happening - i.e. it does not cause execution to continue from the statement after its argument block. In order to do that, one must use exception handling (i.e. use #on:do: instead of #ifCurtailed:). The intention of the #safePrintString example above should actually be expressed as:
[self printOn: stream] on: Error do: [ :e |
nextPutAll: 'an invalid ';
display: self class].
"Will always get to here, even if #printOn: throws an Error"
Note also that unwind protection blocks are not run when the exception mechanism is searching for a handler, only when a handler is actually found and evaluated. If there are no handlers in the call stack, then a walkback will appear, and the unwind protection blocks will not at that stage have been run. They will not be run until the Terminate button on the walkback dialog is pressed, or one terminates the process from the debugger. This does mean that it is possible to construct situations where the system may be left in an invalidate state when the walkback appears if, for example, a global resource is left locked when an exception is raised.
#ensure: is similar to #ifCurtailed: (and is implemented using the same mechanism - see the methods in BlockClosure), but always evaluates the argument block.
In summary, unwind protection (#ifCurtailed:, #ensure:) is not exception handling. Should one wish to guarantee that certain operations are always performed (cleaning up after oneself), but not want to actually intercept and handle exceptions (i.e. one doesn't want to prevent a walkback), then unwind protection should be used. If, on the other hand, one actually wants to handle the errors and continue, then exception handling should be used.