Exceptions: Difference between revisions
Line 376: | Line 376: | ||
As with most method descriptions, the <var>Throws</var> clause on a method declaration | As with most method descriptions, the <var>Throws</var> clause on a method declaration | ||
and definition must match exactly. | and definition must match exactly. | ||
That is, the following is <i><b>invalid</b></i>: | That is, the mismatch in the following definition and declaration is <i><b>invalid</b></i>: | ||
<p class="code">class house | <p class="code">class house | ||
public | public | ||
Line 385: | Line 385: | ||
end public | end public | ||
... | ... | ||
**** The following produces a compile error **** | |||
subroutine paint(%color is string len 6) - | subroutine paint(%color is string len 6) - | ||
throws tackyColor | throws tackyColor |
Revision as of 00:45, 16 November 2011
Exceptions are a technique for handling unusual occurrences in the execution of a method call. This page discusses Janus SOAP 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 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's 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.
Sirius Mods 7.2 introduces an alternative to output parameters to handle unusual cases in method calls. This alternative is called exception handling.
Errors unsuitable for exception handling
Exception handling consists of three parts:
- 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.
- The ability to indicate an unusual or exception situation. This is referred to as throwing an exception.
- The ability to detect a thrown exception and to perform special processing for the exception. This is known as catching an exception.
The Janus SOAP ULI exception handling support 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. Janus SOAP ULI 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, Janus SOAP ULI 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.
User Language is an application development language. While it is theoretically possible to build a program environment in User Language, or even an operating system, this is not really the focus of User Language, and it is not likely anyone would ever do this. The User Language programming environment (Model 204, which is not, itself, written in User Language) 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 Janus SOAP ULI does not turn into exceptions, and thus are not trappable, fall into two broad categories:
- 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.
- 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 Language methods.
Note that Model 204 does provide User Language 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 User Language 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 maintained by the Janus SOAP ULI environment, and there can be User Language exception classes, which are defined and maintained by User Language 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 Language 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:
-
<dtInvalidHexData
- Describes an error in converting hexadecimal data to some other datatype. <dtInvalidRegex
- Describes an error processing a regular expression. <dtDaemonLost
- 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 User Language 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 User Language 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 Janus SOAP ULI implementation of exceptions, exceptions can only be thrown by methods.
In both system and User Language 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 Language 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 Language class name with the same name as the system class name. Of course, it's 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
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 so 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 User Language 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 illustrates 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.
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 illustrate 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 User Language statement on the same line, as in:
try %binary = %input:hexToString catch invalidHexData %binary = '???' end try
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.
Some differences with other languages
This section describes some differences between the Janus SOAP ULI implementation of Try/Catch and implementations in other languages. Outside of these differences, Try/Catch support in Janus SOAP ULI 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 User Language environment is not written in User Language, so
there is no need to propagate exceptions to some outer User Language 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.
Also, automatic propagation of errors is 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 User Language, you can simply catch it and re-Throw it:catch tackyColor to %yuck
- let caller deal with this
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 User Language.
- Catches 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 Catches that correspond to the thrown exception. The Janus SOAP ULI 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 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 Catches are required for base and extension classes, specify the extension class Catches 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 Catches 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 Catches that correspond
to a thrown exception are found for the inner-most Try block, then the outer-most
Try block's Catches 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 Catches 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
, in the following:
try %dmn:run('NASTY') %str = %hex:hexToString catch daemonLost %str = %errorhex:hexToString catch invalidHexdata %str = '???' end try
if %errorhex
had invalid hexadecimal data in it, the Catch InvalidHexData
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, Janus SOAP ULI special-cases two method names in a User Language 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 Super 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.