Exceptions

From m204wiki
Revision as of 22:19, 23 September 2014 by JAL (talk | contribs) (→‎Try block syntax)
Jump to navigation Jump to search

Exceptions are a technique for handling unusual occurrences in the execution of a method call. This page discusses SOUL exception handling.

Background

A wide variety of errors can occur inside a program. Many of these, such as syntax errors, or invalid variable or member names, can be caught at compile-time, so they produce compile-time errors. Other errors can only be caught at run-time, so they produce run-time errors.

For example, a reference to a member of a null object variable will produce a request cancelling “reference to null object” error. Generally, such an error is indicative of a logic error in the program — most likely the program forgot to create an instance of the object, forgot to assign it to the object variable being used, or is mistakenly using the wrong object variable.

In any case, since such an error is indicative of a program logic error, the request cancellation is a benefit: it stops the program at the earliest point that the error was detected so that the program does not do damage because of the error. In addition, if SirFact is being used, the request cancelling error will produce a SirFact dump at the time the error was first detected (or at the very least, indicate the procedure and line number where the error was first detected). Such early detection of error greatly simplifies problem diagnosis.

Still other errors are actually quite common and might not even be considered errors in most contexts in which they occur. For example, the Stringlist class Locate method might not locate any items that match the search criterion. Since this is likely not to be a true error, the Locate method simply returns a 0 in such a case. The Locate method caller can then check for a zero value and do processing appropriate to the no-matching-item-found case.

If the Locate method caller assumes that the Locate always finds a matching item, it is likely to use the zero returned by the Locate method to reference a matched item, then get a request cancelling error when item zero is requested. This illustrates a general principle that methods that can produce unusual results or, at least, results that are significantly different from others, should try to set the unusual or result values to something that is likely to cause a request cancelling error if misused.

For example, a method that returns an object instance might return a Null object under certain error conditions that are not necessarily indicative of a programming error. This allows the calling program to check for a Null and perform appropriate processing if a Null is returned. If the program doesn't expect this error case, it might not check for a Null. If the program is correct, and the error case cannot happen in the given context, the program behaves correctly with no unnecessary error checking. If, however, it is incorrect, and a Null can be returned, it is likely to get a Null-object request-cancelling error when it attempts to use the Null as an object reference.

This is the best of all worlds — no error checking is required if the error is known not to occur in a context, but if that knowledge is incorrect, a request canceling error will quickly stop the program before any damage is done.

Unfortunately, there are some cases where the return value cannot be used to indicate an unusual situation:

  • There is no return value. For example, an error happens inside a subroutine or inside the Set method of a property.
  • The return value has no invalid values available to be used to indicate an unusual situation. For example, if a function returns a string, and a null value is sometimes a valid output, there might be no reasonable string value that can be used to indicate the error.
  • Even if there is a reasonable value (such as a null string, Null object reference, or zero or negative numeric value), such a value might not provide enough information about the nature of the error for the calling program to deal with the error. Obviously, a single string or numeric value, or a Null object reference, cannot provide a lot of information. This is especially important if a method might encounter more than one unusual situation, and the unusual situations are different enough that one might be expected in a particular context, and the other not; or the unusual situations require very different corrective action. For example, a method places an order for a product, and given a product number, it wants to indicate an error if the product number is invalid. The number might be invalid because the product number doesn't exist or because the product is out of stock. Obviously, these are two very different errors, the former possibly being indicative of a programming error, the latter less likely to be.

One solution to such cases is to have an output parameter on the method calls. Unfortunately, this has several problems:

  • Output parameters can be confusing in code. There's nothing that marks them as output parameters, so it's not obvious that's what they are. Also, someone looking for how a variable used as an output parameter was or might be set, might only think to look for assignment statements with the variable on the left side of the assignment. Certainly, life would be easier if that is all one needed to look for.
  • Just because a variable is used for the output parameter, there is no guarantee that the program actually examines the output parameter value. This could mean that where it is incorrectly believed that a certain error situation can't occur, the code might not bother to check the variable used as an output parameter and continue on, oblivious to an error.
  • Output parameters cannot (currently) be optional. This means that a variable has to be specified for the output parameter, even if the unusual situations indicated by the output parameter couldn't happen in a given context. This can be annoying, at best, and confusing, at worst.

An alternative to output parameters to handle unusual cases in method calls is called exception handling.

Errors unsuitable for exception handling

Exception handling consists of three parts:

  1. The ability to define classes of exceptions and the information available for these classes. Unsurprisingly, classes of exceptions are defined in almost the same way as other classes.
  2. The ability to indicate an unusual or exception situation. This is referred to as throwing an exception.
  3. The ability to detect a thrown exception and to perform special processing for the exception. This is known as catching an exception.

Exception handling in SOUL is very similar to many other object-oriented programming languages, such as Java or VB.Net. However, each language's implementation of exception handling is unique, each with subtle or not so subtle differences from others. SOUL is no exception (no pun intended), and significant departures from the way other languages support exceptions is indicated where applicable.

A philosophical departure from the support of most other languages for exceptions is worth noting: while many object-oriented languages make all (or almost all) errors exceptions, SOUL does not. This is partially because many object-oriented languages are not, strictly speaking, application languages — in addition to writing applications in some other object-oriented languages, one might write programming environments or even operating systems. As such, a programming environment or operating system must be able to intercept all errors and try to deal with them, so the object-oriented languages facilitate this by making all errors catchable.

SOUL is an application development language. While it is theoretically possible to build a program environment in SOUL, or even an operating system, this is not really the focus of SOUL, and it is not likely anyone would ever do this. The SOUL programming environment (Model 204, which is not itself written in SOUL) is responsible both for the ultimate catching of errors and the providing of information about the nature of the errors so that they can be corrected.

The kinds of errors that SOUL does not turn into exceptions, and thus are not trappable, fall into two broad categories:

  1. Pernicious environmental errors from which it would generally be almost impossible to recover gracefully on an application level.

    For example, if CCATEMP fills up, it is exceedingly unlikely that a request could recover gracefully. Since CCATEMP is used for so many different things (including record sets for finds, stringlists, collections, internal storage of objects swapped out of a server, transaction backout logs, and so on), it would be very difficult for an application to anticipate all the places that CCATEMP could fill. And even if it could anticipate the places, it would be very difficult to avoid doing anything that might also require CCATEMP.

    Similarly, a PDL (push-down-list) overflow can be detected at almost any method call, not to mention dozens of internal PDL-intensive routines (such as, say, journal output). As such, it would be almost impossible for an application to anticipate all the places where a PDL overflow might be detected. And even if it could anticipate the places, any method calls to handle the situation would likely also encounter a PDL overflow.

  2. Errors that are indicative of logic errors in a program.

    For example, a class member reference via a Null object variable is usually indicative of a programming error. Similarly, a reference to an invalid collection item number is also generally indicative of a programming error.

    While there might be error-causing cases of "sloppy" references to object variables or collection items that you would want to simply catch when they happen:

    • Most of these are handled well by existing facilities such as Auto New and also UseDefault processing for collections.
    • In the odd cases where such sloppy references are useful, it is trivial and efficient to check for the sloppy references (Null object variable, invalid collection item number), so an exception-handling paradigm adds little value.

      Of course, there are always border-line cases where an error that is nearly always indicative of a programming error, might not be in certain contexts. Since the initial Sirius Mods support for exceptions was not accompanied by a complete survey of all error returns by all system methods, it is quite likely that many request cancelling errors in system methods will gradually be converted into catchable exceptions over time. This is also quite likely to be the case for many errors in user-written methods.

Note that Model 204 does provide SOUL facilities to catch even these types of untrappable errors. These facilities include On units, especially On Error units, and APSY error procedures. These facilities make it possible for SOUL programs to, at least, provide nicer notification of an error to a client (web browser, 3270 user, SOAP client) than a broken connection or a Model 204 error message.

Even these facilities, however, are limited by severe environmental resource constraints. For example, if a CCATEMP or record-locking-table full error drives an APSY error procedure or an On Error unit, there might be little these error routines can do without bumping into the very same resource constraints that caused the problem in the first place. This problem is not unique to Model 204 — severe environmental constraints can wreak havoc with the best laid error recovery plans in any environment.

Exception class definitions

Most error conditions have information associated with them. This information can be useful for correcting or recovering from the problem. For example, if an invalid character is detected in a stream of data, it might be useful to know the character offset in the stream, the actual invalid character, and some indication of why the character is invalid. It might also be useful to have a text description of the nature of the error.

The collection of data describing an error situation might be used immediately after the error is detected, or it might be examined elsewhere. As such, this error data should be able to persist indefinitely. Given this requirement, it is clear that the logical place to hold error information is inside an object. And the description of the data associated with a particular error would, of course, be a class.

Classes that describe trappable error situations are called exception classes. To a large degree, exception classes are no different from any other class:

  • They can have Public, Private, and Shared blocks; they can have variables, methods, and the same Allow and Disallow rules as any other class.
  • There can be system exception classes, which are provided as part of the Model 204 installation, and there can be exception classes which are defined and maintained by SOUL code.
  • Exception classes can be used wherever non-exception classes are used, although the reverse is not true. That is, there are certain statements that require an exception class or an object of an exception class. This is largely to prevent accidental misuse of a non-exception class in an exception class context.

User-defined exception classes are denoted by using the Exception keyword in the class header:

class tackyColor exception

In many object-oriented languages, a class is indicated as an exception class by extending some special base class, often called "Exception." One can think of the Exception keyword on the Class declaration as indicating that the class extends some hidden base class. This neither adds nor detracts from an understanding of the basic concept of an exception class.

If you want, you can put the Exception keyword on almost all class declarations; specifying it removes no capabilities from the class. Doing so, however, is misleading, since most classes are likely never to be used to indicate error situations. In this regard, you can view the Exception keyword on a class declaration as documentation that a class can be used as an exception class. While the compiler cannot ensure that a class with an Exception declaration will be used as an exception, it does ensure that a class not declared as Exception is not used in exception contexts.

The one case where the Exception keyword is not allowed on class declarations is in declarations that extend non-exception classes — exception classes can only extend other exception classes.

Some examples of system exception classes are:

InvalidHexData
Describes an error in converting hexadecimal data to some other datatype.
InvalidRegex
Describes an error processing a regular expression.
DaemonLost
Describes a situation where the thread doing processing for a Daemon object was lost (logged off).

This list is far from exhaustive, but it illustrates that system exception classes might be associated with a specific system class. For example, the DaemonLost class is clearly associated with the Daemon class. On the other hand, an exception class might be associated with a facility that's used by many different classes. For example, the InvalidRegex class is not associated with any specific class, because regular expressions are used in Stringlist and Intrinsic methods.

For links to the descriptions of the individual system exception classes, see the "Lists of classes and methods".

Throwing exceptions

When an error situation occurs, the code that detects the situation can do one of two things:

  • It can cancel the request. In SOUL this is done with an Assert statement. In system code, this is done by some equivalent of the Assert statement. This is the correct response to an error if the error is clearly indicative of a programming error or a severe environmental constraint that is likely to make request continuation unproductive.
  • It can throw an exception. In SOUL this is done with a Throw statement. In system code, this is done by some equivalent of the Throw statement. This is the correct response to an error if the error might occur in the normal course of processing and the code that called the method might be able to recover from the error.

    Note: In the SOUL implementation of exceptions, exceptions can only be thrown by methods.

In both system and user-written methods, a thrown exception can only be handled (caught) by the code that called the method. This is different from many other languages' implementations of exceptions where exceptions can be handled locally, that is, inside the same method that threw the exception. Like many other languages, the exceptions that might be thrown by a method must be documented in the method header. This is because a method's exceptions are part of the method's interface to its callers, as much as any input and output parameters, as much as output values.

Specifying a Throws clause

The exceptions thrown by a method are indicated by the Throws clause in the method declaration and definition header. For example, if the method Paint might throw a TackyColor and InvalidHexData exception, it should be declared and defined as:

subroutine paint(%hexColor is string len 6) - throws tackyColor and invalidHexData

Of course, while the keyword And is used to separate the exceptions that might be thrown by a method, the method can only ever throw one exception at a time, and in most cases, it will not throw an exception at all.

The list of exceptions after a Throws keyword is the list of exception class names. These class names could be user-defined exception class names, or they could be system exception class names. If the class is a system class name, it could be fully qualified with the System: namespace indicator. For instance, the previous example could be written as

subroutine paint(%hexColor is string len 6) - throws tackyColor and system:invalidHexData

This would only be necessary if there was a user-defined class name with the same name as the system class name. Of course, it is best to avoid such a situation as much as possible.

As with most method descriptions, the Throws clause on a method declaration and definition must match exactly. That is, the mismatch in the following definition and declaration is invalid:

class house public ... subroutine paint(%color is string len 6) - throws tackyColor and outOfPaint ... end public ... **** The following produces a compile error **** subroutine paint(%color is string len 6) - throws tackyColor ... end subroutine ... end class

It is also invalid if, in the method definition, the list of thrown exceptions contains the same exceptions as the method declaration, but in a different order.

Methods that implement an overridable method cannot throw any exceptions not thrown by the implemented method; however, they do not necessarily have to throw all the exceptions thrown by the implemented method. That is, the exceptions in a Throws list on an Implements method must be a subset of the Throws list on the corresponding Overridable (or Abstract) method. For example, if an overridable method in class Products indicates:

subroutine buy(%productCode is string len 8) - overridable - throws tooExpensive and outOfStock

then it is valid for an implementing method to indicate:

subroutine buy(%productCode is string len 8) - implements buy in products - throws tooExpensive

or even:

subroutine buy(%productCode is string len 8) - implements buy in products

However, it is not valid for the implementing method to throw an exception other than TooExpensive or OutOfStock. This is because a caller of the base class only expects the Buy method to throw one of these two exceptions. It would be surprising, indeed, if the method threw some different exception, simply because it had been overridden.

Using the Throw statement

Once a method declaration indicates the exceptions it might throw, that method could then throw the exception with the Throw statement. The Throw statement must be followed by an instance of the exception class being thrown. For example, if the method header contains

subroutine paint(%color is string len 6) throws tackyColor

and there is a variable declaration in the method:

%tacky is object tackyColor

The method could do this:

throw %tacky

Of course, the Throw statement is likely to be inside an If clause, since exceptions are generally thrown in unusual situations, not common ones:

if %color eq %yuckyGreen or - %color eq %grottyOrange then throw %tacky auditText Threw an exception end if

Note that in the above example, the AuditText statement after the Throw will never be executed. This is because the Throw either returns immediately to the method caller (if the method caller catches the exception), or it cancels the request immediately.

Another problem with the above example is that %tacky was likely to never have been set to reference a TackyColor instance. So, more correct would be something like:

if %color eq %yuckyGreen or - %color eq %grottyOrange then %tacky = new throw %tacky end if

But, even this is not quite right. Usually, the exception objects will contain information to aid in problem determination and recovery:

if %color eq %yuckyGreen then %tacky = new %tacky:reason = 'Yucky' %tacky:alternative = '00FF00' throw %tacky end if if %color eq %grottyOrange then %tacky = new %tacky:reason = 'Grotty' %tacky:alternative = 'FFA500' throw %tacky end if

This example illustrates the point that it is common for any Throw of a particular exception class to always return more or less the same information. As such, exception classes often have constructors that can specify all the information provided by the class. Then, no variables of the exception class need to be defined; the result of the constructor call can simply be thrown:

if %color eq %yuckyGreen then throw %(tackyColor):new('Yucky', '00FF00') end if if %color eq %grottyOrange then throw %(tackyColor):new('Grotty', 'FFA500') end if

Of course, for code readability, named parameters are preferable on such constructors:

if %color eq %yuckyGreen then throw %(tackyColor):new(reason='Yucky', alternative='00FF00') end if if %color eq %grottyOrange then throw %(tackyColor):new(reason='Grotty', alternative='FFA500') end if

An attempt to throw an exception object whose class does not match one of the classes listed in the method declaration's Throws clause results in a compilation error.

Exception classes extending other exception classes

Exception classes can extend other exception classes. As such, the class of an object specified in a Throw statement does not have to match any class in the method's Throws list exactly — it can be of an extension class of one of the Throws list classes. Because an extension class can, itself, be extended, and because of multiple inheritance, this means that a Thrown exception object might match multiple classes in a method's Throws list.

The thrown (and therefore catchable) class is the first class in the Throws list that matches the object in the Throw. That is, the first class in the Throws list that exactly matches the thrown object's class or that is a base class of the thrown object is used as the thrown class for the method caller. Because of this, a Throws list must always specify extension classes before base classes. Otherwise, the base class in the Throws list would always match any object that would match an extension class, so the extension class would never be used as the thrown class.

For example, if exception class ReallyNastyColor extended exception class TackyColor, and method Paint had this header:

subroutine paint(%color is string len 6) throws tackyColor

And if the following statement appeared inside the Paint method;

throw %(reallyNastyColor):new(reason='Awful')

The new ReallyNastyColor exception object would be thrown as a TackyColor object. That is, the callers of Paint would not be able to assign the thrown exception to a ReallyNastyColor object without the use of a narrowing assignment.

Try and Catch

Without any action on a method caller's part, a thrown exception is, for all intents and purposes, a request cancelling error. To prevent the request cancellation, an exception must be "caught." This is achieved by the use of a Try/Catch block.

A Try/Catch block consists of two parts. The first, the Try section, contains one or more SOUL statements that might result in an exception. The Try section is then followed by one or more Catch sections, each one of them handling (catching) a particular class of exception.

The following fragment shows the use of a Try/Catch block to trap an exception caused by invalid hexadecimal data:

try %binary = %input:hexToString catch invalidHexData %binary = '???' catch invalidBase64Data %binary = '???' end try

This example illustrates a few points:

  • The end of the statements whose exceptions are being caught is indicated by the first Catch block.
  • The class of exceptions being caught follows the Catch statement.
  • A catch block is terminated by another Catch statement or an End Try.
  • One can catch multiple exception types, each within its own Catch block.
  • There is no validation that the type of exception being caught might actually be thrown inside the Try block. In the preceding example, there would be no chance for an InvalidBase64Data exception inside the Try block since the HexToString method will not throw such an exception. Having a block to catch an exception that's not possible is no different from having an If/Else If block for an impossible condition — it simply adds some dead code to a request.

Try block syntax

Try tStmt1  ; * Exceptions can [ tStmt2 ]  ; * be caught in [ ... ]  ; * these statements [ Rethrow rethClassA [And rethClassB ] ... ] ... any number of Rethrow statements [ Catch catClassC [To %objC] [And catClassD [To %objD]] ... [ cStmtW ]  ; * These are executed if any [ cStmtX ]  ; * of the execptions on the [ ... ]  ; * Catch statement are thrown ] ... any number of Catch blocks [ Success [ sStmtY ]  ; * These are executed [ sStmtZ ]  ; * if no execptions [ ... ]  ; * are thrown ] End Try

Syntax terms

The Try block is divided into two or more sections, which are separated by Rethrow, Catch, and Success statements. Multiple Rethrow and Catch statements can be mixed with each other and with the Success statement, in any order.

There must be either one Rethrow or Catch statement in a Try block.

tStmt1 [tStmt2...] The first section in the Try block must contain one or more SOUL statements. These statements are executed in turn, and if an exception is thrown within them, execution resumes at the Rethrow or Catch statement specifying that exception; if there are none, the request is cancelled. If no exceptions are thrown, execution resumes with the Success block statements, if any, and then after the End Try statement.
Rethrow rethClassA ... The Rethrow statement (not block) specifies one or more exception classes; if one of them is thrown, then the method containing the Try block throws that exception. Hence a Rethrow statement can only occur within a method which declares all of the rethClass exceptions in its Throws clause.

Each rethClass may not be the same as, nor an extension of, any exception class preceding it on the statement or on preceding Rethrow and Catch statements in the Try block.
Catch catClassC ... The Catch statement specifies one or more exception classes; if one of them is thrown, then the (optional) cStmt statements in the Catch block are executed in turn, and then execution resumes after the End Try statement.

Each catClass may not be the same as, nor an extension of, any exception class preceding it on the statement or on preceding Rethrow and Catch statements in the Try block.
Success If no exceptions are thrown in the tStmt statements, the (optional) sStmt statements in the Success block are executed in turn, and then execution resumes after the End Try statement.

Try block considerations

A Try block can contain more than one statement:

try %binary = %input:hexToString %bin = %input64:base64ToString %key = %inputKey:hexToString %foo:process(%binary, %bin, %key) catch invalidHexData %binary = '???' catch invalidBase64Data %bin = '???' end try

While valid, this example is intended to show that it might not be a good idea to put a lot of statements inside a Try block:

  • It obscures which statements might be throwing which exceptions, and this makes the code harder to read.
  • The more statements you have inside a Try block, the more likely it is that you will accidentally catch an exception in a statement in which you were not expecting an exception. For example, in the above code, the InvalidHexData Catch block is clearly fixing a problem with invalid hexadecimal data in %input. But, it will also catch an exception thrown by invalid hexadecimal data in %inputKey, and in this case, probably it will do the wrong thing.
  • All the code in the Try block after the method that threw the exception will not be executed. While, in some cases, this is what would be intended, there are many cases where this would not be intended. In the above example, it is likely that one would want to try to execute the Base64ToString method on %input64 after "correcting" errors in executing HexToString on %input.

As a general rule of thumb, place as few statements inside a Try block as possible. To facilitate this, you can follow the Try statement by a SOUL statement on the same line, as in:

try %binary = %input:hexToString catch invalidHexData %binary = '???' end try

Referencing a thrown exception object

As noted in "Using the Throw statement", what gets thrown with an exception is an exception object that contains information about the nature of the exception. In the Try/Catch examples to this point, the thrown objects were ignored — only the class of the thrown exception was used. If you want to reference the thrown exception object, you must specify a To clause, followed by an object variable of the exception class being caught.

For example, if %daemon is a Daemon object and %daemonLost is an object of the DaemonLost exception class, the following block catches the exception thrown if the daemon thread was logged off for some reason, and it displays the output of the last command up to the point where it logged off:

try %daemon:run('INCLUDE NASTY') catch daemonLost to %daemonLost printText Daemon died! Its last words were: %daemonLost:daemonOutput:print end try

Presumably, this block would be useful in diagnosing the problem or even correcting it.

Rethrow

Sirius Mods 8.1 introduced the Rethrow clause in a Try block. Rethrow lets you propagate an exception without having to assign it to a local variable, probably eliminating the need to declare a local exception class object for the sole purpose of propagating an exception.

The Rethrow clause must occur at the same level as a Catch clause in a Try block. But, unlike the Catch clause, no code is allowed "inside" the Rethrow "block." Because Rethrow causes control to pass immediately out of the current method, no code after Rethrow would ever be executed.

A Rethrow clause can only be followed by an End Try, a Catch clause, or a Success block. Like the Throw statement:

  • The Rethrow clause must be invoked inside a method.
  • The class or classes specified on a Rethrow clause must be declared as being thrown by that method.

The following local subroutine illustrates a Rethrow of an InvalidHexdata exception. This subroutine sends a string converted from hex to binary on a socket.

local subroutine (socket):sendHex(%hexdata is longstring) throws invalidHexData %sendData is longstring try %sendData = %hexdata:hexToString rethrow invalidHexdata end try %this:send(%sendData) end subroutine

Mutiple classes on a Catch or Rethrow statement

Sirius Mods 8.0 introduced the ability to specify multiple exception classes on a single Catch statement. The class names are separated from each other by the And keyword. However, in Sirius Mods 8.0, no To clause could be specified on a Catch statement with multiple classes.

Sirius Mods 8.1 added support for To clauses on a Catch statement with multiple classes as well as including support for multiple classes on the Rethrow statememt.

The following illustrates a Catch statement with multiple classes:

try %foo:doSomethingCrazy catch invalidHexdata to %ivhexData and tooCrazy and notCrazyEnough to %notCrazyEnough end try

If the above were inside a method, the exceptions could be rethrown:

try %foo:doSomethingCrazy rethrow invalidHexdata and tooCrazy and notCrazyEnough end try

Of course, if a Catch block catches multiple class exceptions, it might be important to determine which exception was actually thrown. In that case, a null test can be performed on the target objects:

try %foo:doSomethingCrazy catch invalidHexdata to %ivhexData and and tooCrazy and notCrazyEnough to %notCrazyEnough ... if %ivhexData is not null then ... elseIf %notCrazyEnough is not null then ... end if ... end try

For this to work, one must be sure that the target catch objects are null before the Try block. And, unless there is a lot of common code for the classes in the Catch block, it probably makes more sense to just have a separate Catch block for each exception.

There is no difference between rethrowing exceptions in a single Rethrow statement and multiple Rethrow statements, so it is largely a matter of style as to which approach is used.

Success blocks

In cases where a Try block contains multiple statements, a Success block makes it clear in the code which statement is expected to produce the exceptions that are being caught. These blocks also protect you from an inadvertent exception thrown in an unexpected context.

For example, consider the following scenario. You want to try statement <a> and, if no exceptions get Thrown, you want to do statements <b>, <c>, <d>, and <e>. Otherwise, if statement <a> throws an exception, you want to do statements <x>, <y>, or <z>, depending on the exception.

You code your Try/Catch block like this:

try <a>... <b>... <c>... <d>... <e>... catch foo <x>... catch bar <y>... catch another <z>... end try

If statement <a> does indeed throw an exception, statements <b> through <e> do not run, and the appropriate Catch statement takes effect. However, if statement <a> does not throw an exception, there might be no way to know that statement <b>, <c>, <d>, or <e> might throw an exception that is one of the exceptions in the subsequent Catch statements. Or you might be aware of their capacity to do so, but you might not expect an exception from any of them in this context. Prior to Version 7.8 of the Sirius Mods, there was no good way of preventing the catches to also be in effect for these statements as well as for statement <a>.

As of Sirius Mods 7.8, a Success block inside the Try block resolves the problem by making it clear that the Catch statements do not apply to statements <b>, <c>, <d>, and <e>:

try <a>... success <b>... <c>... <d>... <e>... catch foo <x>... catch bar <y>... catch another <z>... end try

The principle benefits of the Success statement are:

  • It makes it clear in the code which statement is expected to produce the exceptions being caught.
  • It prevents a catch from accidentally catching an exception from a statement that didn't really expect that exception.

You can also reverse the order of the the Success and catches:

try <a>... catch foo <x>... catch bar <y>... catch another <z>... success <b>... <c>... <d>... <e>... end try

Some differences with other languages

This section describes some differences between the SOUL implementation of Try/Catch and implementations in other languages. Outside of these differences, Try/Catch support in SOUL is very similar to that in other languages.

  • Unlike Java, it is not necessary to provide a Catch for all exceptions that a method might throw. This seems antithetical to the idea that exceptions are unusual conditions and, in many instances, are known to be impossible. It seems unnecessary to add code to deal with (or ignore) an error that cannot occur in a context.
  • Unlike many other languages, uncaught exceptions are not automatically propagated to higher level callers. Partially, this is because the SOUL environment is not written in SOUL, so there is no need to propagate exceptions to some outer SOUL environment which, presumably, would clean up the failing request and possibly provide diagnostics about the error. Instead, clean-up and diagnostics are provided by the assembler environment.

    Automatic propagation of errors is also a grand opportunity for bugs. Since an automatically propagated error can happen anywhere in a method, there is no indication that the writer of a method considered the possibility of the propagated error. Updates to data-structures, including transactions that update files, might be half-done at the time of the occurrence of an exception that gets automatically propagated.

    While languages that provide automatic exception propagation usually also provide a Finally clause to "ensure" that the method doesn't leave things in a half-done state, there is nothing in a Finally clause that indicates whether the programmer anticipated the particular error being propagated. And the absence of a Finally clause does not prevent error propagation, anyway.

    If you want to propagate an exception in SOUL, you can simply catch it and re-Throw it:

    catch tackyColor to %yuck * let caller deal with this throw %yuck

    This makes it clear which error is being propagated and under what circumstances.

  • The absence of automatic exception propagation eliminates the utility of a Finally clause, so no Finally clause is available in Try/Catch blocks in SOUL.
  • Catch statements cannot catch locally thrown exceptions. That is, a Throw statement always results in immediate exit from the current method, regardless of whether or not the Throw is inside of a Try block and whether or not there are Catch statements that correspond to the thrown exception. The SOUL view of exceptions is that they are part of the interface between a method and its callers, so they have no place in controlling local program flow.

Nesting Try/Catch blocks

Like in other languages, Try/Catch blocks can be nested. That is, a Try/Catch block can be inside the try or catch clause of another Try/Catch block:

try %dmn:run('I STEP1') try %str = %hex:hexToString %dmn:run('I STEP2 ' with %str) catch invalidHexdata %dmn:run('I STEP2 ???') end try catch daemonLost auditText My daemon's gone! try %str = %hex:hexToString catch invalidHexdata %str = '???' end try return %str end try

The Catch that applies to a particular thrown exception is the first Catch that either exactly matches the class of the thrown exception, or that is a base class of the thrown exception. Because of this, it is possible to catch many different exceptions with a single Catch if all the exceptions are extensions of a specified base class. For this reason, too, it is invalid to specify a Catch for an exception class after a Catch for a base class of that exception class, since the base class Catch will always catch the exception class exception before the exception class Catch is processed. If separate Catch statements are required for base and extension classes, specify the extension class Catch statements first.

Because there is no single base exception class for all exceptions (as there is in many other languages), however, it is not possible to generically catch any and all exceptions with a single Catch. While this ability might be convenient, it is also likely to encourage sloppy programming, where generic Catch statements obscure the possible errors in a context, and make it all too easy to catch unanticipated errors and do the wrong thing.

For Try blocks nested inside of other Try blocks, if no Catch statements that correspond to a thrown exception are found for the inner-most Try block, then the outer-most Try block's Catch statements are checked for a match, and they are used if a match is found. That is, when Try blocks are nested, all Try blocks are in effect for statements inside the inner-most Try block. Note, however, that other Catch statements for a Try block are no longer in effect inside any of the Catch blocks for the Try. For example, if a request had a Daemon object %dmn, and if %errorhex had invalid hexadecimal data in it in the following block:

try %dmn:run('NASTY') %str = %hex:hexToString catch daemonLost %str = %errorhex:hexToString catch invalidHexdata %str = '???' end try

The Catch InvalidHexData above would not catch the conversion error, because the conversion is not inside the Try block associated with that Catch — it is in a Catch block.

OnThrow and OnUncaught

An exception class might want to perform special processing at the time an exception is thrown:

  • It might want to make sure the exception object has valid data.
  • It might want to record diagnostic information, perhaps to the audit trail or perhaps to some Model 204 file.
  • It might want to derive some variable values that might not necessarily have been derivable in the Constructor.

To provide this capability, SOUL special-cases two method names in a user-defined exception class: OnThrow and OnUncaught. Both of these methods must be Subroutines (as opposed to Functions or Properties) and cannot have parameters.

The OnUncaught subroutine is automatically called whenever an exception of the containing class is thrown, and the exception will not be caught. The OnThrow subroutine is automatically called whenever an exception of the containing class is thrown, and either the exception will not be caught and there is no OnUncaught method in the class, or the exception will be caught.

These two method names have no meaning in non-exception classes.

These methods can be called explicitly, and they can be either Private or Public (though whether they are Public or Private is irrelevant for implicit calls when an exception is thrown).

The following illustrates an OnThrow subroutine that makes sure the exception data is valid at the time an exception is thrown:

class pratfall exception public variable sound is string len 32 subroutine onThrow end public subroutine onThrow assert %this:sound eq 'splat' or - %this:sound eq 'boing' end subroutine end class

The following illustrates an OnUncaught subroutine that logs information from the exception to the audit trail before allowing the request to be cancelled:

class pratfall exception public variable sound is string len 32 subroutine onUncaught end public subroutine OnUncaught auditText Taking a pratfall -- {%this:sound} end subroutine end class

If SirFact is available and capturing dumps for requesting cancelling errors, all the information one would need is likely to be in the dump, so there is probably little need to collect extra data in an OnUncaught subroutine.

There is no way for an OnUncaught or OnThrow subroutine to undo the effect of the exception, that is, to prevent a request cancellation if the exception is uncaught. Both routines, however, can force a request cancellation, perhaps by using an Assert statement, even if the exception would have been caught. If a request cancellation occurs inside on OnThrow subroutine for an exception that's about to be caught, the catching statements are not executed, because the request is cancelled before the OnThrow subroutine returns.

The Throws clause is invalid on an exception class's OnThrow and OnUncaught subroutines, so these subroutines cannot, themselves, throw an exception (for, hopefully, obvious reasons).

The Overridable and Implements clauses are also not valid in an exception class's OnThrow and OnUncaught subroutine declarations. However, their behavior is similar to overridable routines:

  • If an extension exception class contains an OnUncaught or OnThrow routine, that routine will be called, when appropriate, rather than the base class routine.
  • The extension class routine can call the corresponding base class routines as it deems fit. It does this, however, by using the subroutine name (qualified with the class name) rather than with the superclass method (since no overriding is involved):

    subroutine onThrow auditText Calling base class OnThrow %this:(fooError)onThrow auditText returned from base class OnThrow end subroutine

    In this example, the extension exception class's base class is called FooError. Of course, an OnUncaught in an extension class can also call an OnThrow for a base class (or even for its own class).

If an OnUncaught subroutine exists in a base class, but the extension class contains only an OnThrow subroutine, the base class OnUncaught routine will be called if an extension class object is thrown as an exception and the exception will not be caught. That is, an OnUncaught subroutine will always be called for uncaught exceptions if one is available in the exception's class or any base class, regardless of whether there are any OnThrow subroutines in the exception class or any of its base classes.