Exceptions

This wiki page is intended to become part of GNU Smalltalk. Please do not make major changes to it unless you have a proper future changes copyright assignment on file with the FSF, or you intend to file a copyright assignment or disclaimer for your changes with the FSF.

For much Smalltalk code, including your first code, you can use the original Smalltalk-80 error signalling mechanism:

  followPath: aString [
    prefixes with: actions do: [:prefix :action | | index |
        index := aString indexOfRegex: prefix.
        (index notNil and: [1 = index first]) ifTrue:
            [^action value: aString]].
    self error: 'no followPath match for ' , aString.
  ]

In the above code, if a matching prefix is found, the method will answer the result of invoking the respective action. If no prefix is found, Smalltalk will unwind the stack and print an error message including the message you gave and stack information. In this example I gave the string '/hello/there':

Object: Toaster new "<0x4024d7b8>" error: no followPath match for /hello/there
Error(Exception)>>signal
Error(Exception)>>signal:
Toaster(Object)>>error:
Toaster>>followPath:
Toaster class>>toast:
UndefinedObject>>executeStatements

Above we see the object that received the #error: message, the message text itself, and the frames (innermost-first) running when the error was captured by the system. In addition, the rest of the code in methods like Toaster class>>#toast: was not executed.

So simple error reporting gives us most of the features we want:

  • Execution stops immediately, preventing programs from continuing as if nothing is wrong.
  • The failing code provides a more-or-less useful error message.
  • Basic system state information is provided for diagnosis.
  • A debugger can drill further into the state, providing information like details of the receivers and arguments on the stack.

When to use exceptions

Exceptions are a more powerful and complex error handling mechanism. They are like "exceptions" in other programming languages, but are more powerful and do not always indicate error conditions. Even though we use the term "signal" often with regard to them, do not confuse them with the signals like TERM and INT provided by some operating systems; they are a different concept altogether.

Deciding to use exceptions instead of #error: is a matter of aesthetics, but you can use a simple rule: use exceptions only if you want to provide callers with a way to recover sensibly from certain errors, and then only for signalling those particular errors.

For example, if you are writing a word processor, you might provide the user with a way to make regions of text read-only. Then, if the user tries to edit the text, the objects that model the read-only text can signal a ReadOnlyText or other kind of exception, whereupon the user interface code can stop the exception from unwinding and report the error to the user.

When in doubt about whether exceptions would be useful, err on the side of simplicity; use #error: instead. It is much easier to convert an #error: to an explicit exception than to do the opposite.

Creating and signalling exceptions

GNU Smalltalk provides a few exceptions, all of which are subclasses of Exception. Most of the ones you might want to create yourself are in the SystemExceptions namespace. You can browse the builtin exceptions in the base library reference, and look at their names with Exception printHierarchy.

Some useful examples from the system exceptions are SystemExceptions.InvalidValue, whose meaning should be obvious, and SystemExceptions.WrongMessageSent, which we will demonstrate below.

Let's say that you change one of your classes to no longer support #new for creating new instances. However, because you use the first-class classes feature of Smalltalk, it is not so easy to find and change all sends. Now, you can do something like this:

Object subclass: Toaster [
    Toaster class >> new [
        ^SystemExceptions.WrongMessageSent
            signalOn: #new useInstead: #toast:
    ]

    Toaster class >> toast: reason [
        ^super new reason: reason; yourself
    ]

    ...
]

Admittedly, this doesn't quite fit the conditions for using exceptions. However, since the exception type is already provided, it is probably easier to use it than #error: when you are doing defensive programming of this sort.

Using #signal

Signalling an exception is really a two-step process. First, you create the exception object; then, you send it #signal.

If you look through the hierarchy, you'll see many class methods that combine these steps for convenience. For example, the class Exception provides #new and #signal, where #signal's method is just ^self new signal.

You may be tempted to provide only a signalling variant of your own exception creation methods. However, this creates the problem that your subclasses will not be able to trivially provide new instance creation methods.

Error subclass: ReadOnlyText [
    ReadOnlyText class >> signalOn: aText range: anInterval [
        ^self new initText: aText range: anInterval; signal
    ]

    initText: aText range: anInterval [
        <category: 'private'>
        ...
    ]
]

Here, if you ever want to subclass ReadOnlyText and add new information to the instance before signalling it, you'll have to use the private method #initText:range:.

We recommend leaving out the signalling instance-creation variant in new code, as it saves very little work and makes signalling code less clear. Use your own judgement and evaluation of the situation to determine when to include a signalling variant.

Handling exceptions

To handle an exception when it occurs in a particular block of code, use #on:do:

^[someText add: inputChar beforeIndex: i]
    on: ReadOnlyText
    do: [:sig | sig return: nil]

This code will put a handler for ReadOnlyText signals on the handler stack while the first block is executing. If such an exception occurs, and it is not handled by any handlers closer to the point of signalling on the stack (known as "inner handlers"), the exception object will pass itself to the handler block given as the do: argument.

You will almost always want to use this object to handle the exception somehow. There are six basic handler actions, all sent as messages to the exception object:


return:

Exit the block that received this #on:do:, returning the given value. You can also leave out the argument by sending #return, in which case it will be nil. If you want this handler to also handle exceptions in whatever value you might provide, you should use #retryUsing: with a block instead.

retry

Acts sort of like a "goto" by restarting the first block. Obviously, this can lead to an infinite loop if you don't fix the situation that caused the exception.

#retry is a good way to implement reinvocation upon recovery, because it does not increase the stack height. For example, this:

  frobnicate: n [
    ^[do some stuff with n]
        on: SomeError
        do: [:sig | sig return: (self frobnicate: n + 1)]
    ]

should be replaced with retry:

  frobnicate: aNumber [
    | n |
    n := aNumber.
    ^[do some stuff with n]
        on: SomeError
        do: [:sig | n := 1 + n. sig retry]
  ]

retryUsing:

Like #retry, except that it effectively replaces the original block with the one given as an argument.

pass

If you want to tell the exception to let an outer handler handle it, use #pass instead of #signal. This is just like rethrowing a caught exception in other languages.

resume:

This is the really interesting one. Instead of unwinding the stack, this will effectively answer the argument from the #signal send. Code that sends #signal to resumable exceptions can use this value, or ignore it, and continue executing. You can also leave out the argument, in which case the #signal send will answer nil. Exceptions that want to be resumable must register this capability by answering true from the #isResumable method, which is checked on every #resume: send.

outer

This is like #pass, but if an outer handler uses #resume:, this handler block will be resumed (and #outer will answer the argument given to #resume:) rather than the piece of code that sent #signal in the first place.

None of these methods return to the invoking handler block except for #outer, and that only in certain cases described for it above.

Exceptions provide several more features; see the methods on the classes Signal and Exception for the various things you can do with them. Fortunately, the above methods can do what you want in almost all cases.

If you don't use one of these methods or another exception feature to exit your handler, Smalltalk will assume that you meant to sig return: whatever you answer from your handler block. We don't recommend relying on this; you should use an explicit sig return: instead.

When an exception isn't handled

Every exception chooses one of the above handler actions by default when no handler is found, or they all use #pass. This is invoked by sending #defaultAction to the class.

One example of a default action is presented above as part of the example of #error: usage; that default action prints a message, backtrace, and unwinds the stack all the way.

The easiest way to choose a default action for your own exception classes is to subclass from an exception class that already chose the right one, as explained in the next section. For example, some exceptions, such as warnings, resume by default, and thus should be treated as if they will almost always resume.

Selecting by superclass is by no means a requirement. Specializing your Error subclass to be resumable, or even to resume by default, is perfectly acceptable when it makes sense for your design.

Creating new exception classes

If you want code to be able to handle your signalled exceptions, you will probably want to provide a way to pick those kinds out automatically. The easiest way to do this is to subclass Exception.

First, you should choose an exception class to specialize. Error is the best choice for non-resumable exceptions, and Notification or its subclass Warning is best for exceptions that should resume with nil by default.

Exceptions are just normal objects; include whatever information you think would be useful to handlers. Note that there are two textual description fields, "description" and "messageText": description, if provided, should be a more-or-less constant string answered from a override method on #description, meant to describe all instances of your exception class. The messageText is meant to be provided at the point of signalling, and should be used for any extra information that code might want to provide. Your signalling code can provide the messageText by using #signal: instead of #signal. This is yet another reason why signalling variants of instance creation messages can be more trouble than they're worth.

Hooking into the stack unwinding

More often useful than even #on:do: is #ensure:, which guarantees that some code is executed when the stack unwinds, whether because of normal execution or because of a signalled exception.

Here is an example of use of #ensure: and a situation where the stack can unwind even without a signal:

Object subclass: MessageMacro [
  | cancelBlock |

  expandMessageWithLocalReturn: aMessageNode
    "Sets up forgoExpansion before entering the expand protocol,
     forgoing if aMessageNode's parent is a cascade."
    | oldCancelBlock |
    aMessageNode parent isCascade ifTrue: [^aMessageNode].
    oldCancelBlock := cancelBlock.
    ^[cancelBlock := [^aMessageNode].
      self expandMessageInPlace: aMessageNode]
        ensure: [cancelBlock := oldCancelBlock]
  ]

  forgoExpansion [
    "Return from my innermost invocation (on this instance) to the
     tree rewriter without modifying the tree."
    cancelBlock value
  ]

  expandMessageInPlace: aMessageNode [
    ^self subclassResponsibility
  ]
]

This abstract class provides a way to unwind back to the invocation of #expandMessageWithLocalReturn: by sending #forgoExpansion while it is executing. The use of #ensure: guarantees (hence the name "ensure") that even if cancelBlock is invoked or an error is handled by unwinding, the old cancel block will be restored.

The return statement in aMessageNode parent isCascade ifTrue: [^aMessageNode] shows that you have probably already used the unwinding feature of blocks used for the cancelBlock even if you didn't know it.

You have probably been using #ensure: without knowing. For example, Semaphore>>#critical: uses it to ensure that the semaphore is freed when leaving the critical section.

Handler stack unwinding caveat

When using #ensure: blocks with exception handlers, keep in mind that when a handler is invoked, the stack is not unwound. For almost all applications, this will not matter, but it technically changes the semantics significantly so should be kept in mind.

For comparison, this Smalltalk code:

| n |
n := 42.
[[self error: 'error'] ensure: [n := 24]]
    on: Error
    do: [:sig | n printNl. sig return].
n printNl.

will put "42" followed by "24" on the transcript, because the n := 24 will not be executed until sig return is invoked, unwinding the stack. Similar Java code acts differently:

int n = 42;
try
  {
    try {throw new Exception ("42");}
    finally {n = 24;}
  }
catch (Exception e)
  {
    System.out.println (n);
  }
System.out.println (n);

printing "24" twice, because the stack unwinds before executing the catch block.

The Smalltalk exception system is designed this way because it's rare to write code that could break because of this difference, and the #resume: feature doesn't make sense if the stack is unwound. It is easy enough to unwind a stack later, and is not so easy to wind it again if done too early.

Handling multiple exception types

A quick shortcut to handling multiple exception types is the ExceptionSet, which allows you to have a single handler for the exceptions of a union of classes:

^[do some stuff with n]
    on: SomeError, ReadOnlyError
    do: [:sig | ...]

In this code, any SomeError or ReadOnlyError signals will be handled by the given handler block.

Syndicate content

User login