But let's consider error handling in your own subroutines. Let's assume that you're writing a subroutine that takes a piece of data, reformats it in a special way, and then returns the formatted data. You'll call that subroutine Reformat(), and design it so that a caller passes the data to be reformatted, and gets back the reformatted data. And maybe you want to allow a second argument that specifies some "operation" (ie, how to reformat the data). Someone may call it like so:
/* Compress "OriginalData" and store the results in "NewData" */ NewData = Reformat(OriginalData, "COMPRESS")You intend to use the PROCEDURE keyword so that your Reformat subroutine is safe to call from anyplace (ie, it won't quash any variables used elsewhere).
Now let's assume that you want to bulletproof this subroutine. Maybe there are some errors that it may encounter. For example, what happens if someone passes an operation arg that you don't support? And what happens if there is some problem in your algorithm to reformat the data? For example, maybe your Reformat function is written to specifically work on some jpeg format data, and what happens if you determine that the data you have been passed is not a jpeg format? And maybe there are some other errors that could happen as a result of whatever operation your subroutine does. You want to be able to inform the caller that there was an error. How do you do that?
There are a number of techniques you could employ. For example, maybe you could return an empty string if there is any error. So the caller could check for an empty return. But that wouldn't tell him why there was an error, so it's not very informative.
Alternately, you could redesign your function to return an error number or message, and instead save the reformatted data in some specifically named variable. But that hardwires your subroutine to use a specific variable name, so it isn't very general purpose. Alternately, you could require the caller to pass the name of the desired output variable, and you would use the VALUE() function to set its value. But that requires the caller to pass an extra argument. As a third alternative, maybe you'll store the error result in a specifically named "error variable" that the caller is required to check when your subroutine returns. But that means the caller needs to beware not using that same variable name for other purposes.
Of course, you need to return a value no matter what error happens, because otherwise a SYNTAX condition will be raised if you simply RETURN (without a value), and he is attempting to assign that return or use it in an expression.
And with all the above techniques, the caller still has to insert error checking after every call to your function. Let's assume that you've chosen to implement the "error variable" method, and you'll set a variable named "ReformatResult" where 0 means success, and any other value is a specific error. What happens if the caller makes numerous calls to your function, and needs to stop if there is an error at any point? That means the calling code could look something like this:
/* Compress "OriginalData" and store the results in "NewData" */ NewData1 = Reformat(OriginalData1, "COMPRESS") /* Now check for an error */ IF ReformatResult \= 0 THEN SIGNAL ReformatError /* Do another Reformat() and check for error */ NewData2 = Reformat(OriginalData2) IF ReformatResult \= 0 THEN SIGNAL ReformatError /* Do another Reformat() and check for error */ NewData3 = Reformat(OriginalData3, "UNKNOWNOPERATION") IF ReformatResult \= 0 THEN SIGNAL ReformatError RETURN ReformatError: SAY "Error" ReformatResult "in Reformat()" RETURNAnd of course, because of the scheme you've chosen, the caller cannot use the return value without checking for an error. So he can't do something like this:
/* Compress "OriginalData", and then perform a second * reformat operation on that compressed data, and * store the final result in "NewData" */ FinalData = Reformat( Reformat(OriginalData, "COMPRESS"), "REMOVETAGS" )And maybe the caller wants to be able to display an error message too. In that case, the caller either needs to translate your error number to an appropriate message, or maybe you'll use a second variable named ReformatMessage to store any error message.
So maybe the error handling in Reformat() will look like this:
Reformat: PROCEDURE OriginalData = ARG(1) Operation = TRANSLATE(ARG(2)) IF Operation = "" THEN Operation = "COMPRESS" /* Check for an unsupported Operation */ IF Operation \= "REMOVETAGS" & Operation \= "COMPRESS" THEN DO ReformatResult = 1 ReformatMessage = "Bad Operation" RETURN "" END /* Check for the proper data format */ IF LENGTH(OriginalData) = 0 THEN DO ReformatResult = 2 ReformatMessage = "No data supplied" RETURN "" END /* Here, you'd do your reformatting, but * other errors may come up which you'll * have to handle as above. */ /* And of course, before you return, don't * forget to set that ReformatResult * variable, even for success. */ ReformatResult = 0 RETURN NewDataBut there can be a simpler method of reporting and handling errors. Using the RAISE instruction, it is possible for your subroutine to raise a SYNTAX, ERROR, FAILURE, or HALT condition in the caller. As soon as your subroutine uses the RAISE instruction, REXX terminates your subroutine right there on that instruction. It doesn't matter what is returned to the caller because REXX takes care of that. If the caller is trapping the condition that you've raised, control is automatically routed to his handler. So even the caller does not need to check the return value because program control is automatically rerouted.
The RAISE instruction allows you to specify which condition you wish raised: SYNTAX, ERROR, FAILURE, or HALT. (NOTREADY and NOVALUE are not allowed). And after that, you specify any numeric value (of your choice) to return. So let's rewrite our Reformat subroutine to use RAISE. We'll raise a FAILURE condition.
Reformat: PROCEDURE OriginalData = ARG(1) Operation = TRANSLATE(ARG(2)) /* Check for an unsupported Operation. If so, * get out of here right now, raising FAILURE * in the caller (with an error number of 1) */ IF Operation \= "REMOVETAGS" & Operation \= "COMPRESS" THEN RAISE FAILURE 1 /* If the RAISE instruction is done above, then * your subroutine will never get here. REXX will * have ended this subroutine and raised the FAILURE * condition in the caller. */ /* Check for the proper data format If so, * get out of here right now, raising FAILURE * in the caller (with an error number of 2) */ IF LENGTH(OriginalData) = 0 THEN RAISE FAILURE 2 /* Here, you'd do your reformatting, but * other errors may come up which you'll * have to handle as above. */ /* No need to set any error variable */ RETURN NewDataAs you can see, this simplifies your own coding. You've reduced 11 instructions pertaining to error handling down to 2. "But what about an error message?", you may ask. The RAISE instruction allows you to optionally append the keyword DESCRIPTION, followed by whatever literal string you wish returned.
Reformat: PROCEDURE OriginalData = ARG(1) Operation = TRANSLATE(ARG(2)) /* Check for an unsupported Operation */ IF Operation \= "REMOVETAGS" & Operation \= "COMPRESS" THEN RAISE FAILURE 1 DESCRIPTION "Bad Operation" /* Check for the proper data format */ IF LENGTH(OriginalData) = 0 THEN RAISE FAILURE 2 DESCRIPTION "No data supplied" RETURN NewDataJust as easy and still no more instructions.
So now, where the caller calls Reformat, he can use a CATCH FAILURE instruction (perhaps inside of a DO/END) to trap FAILURE condition. And he can call CONDITION('E') to retrieve the number you returned, and CONDITION('D') to retrieve the message. Here's the new code:
/* If any of the following calls fail, we'll * jump to the CATCH FAILURE on that call */ DO NewData1 = Reformat(OriginalData1, "COMPRESS") NewData2 = Reformat(OriginalData2) NewData3 = Reformat(OriginalData3, "UNKNOWNOPERATION") CATCH FAILURE SAY "Error" CONDITION('E') "in Reformat() at line" CONDITION('L') SAY CONDITION('D') RETURN ENDAs you can see, this also simplifies his coding (and makes it more readable) because he no longer has to explicitly check for errors. And so he can do this as well:
FinalData = Reformat( Reformat(OriginalData, "COMPRESS"), "REMOVETAGS" )If the inner call fails (ie, to reformat with the COMPRESS Operation), then the outer call will never be made. Control will have jumped to the FAILURE handler.
By using the RAISE instruction, you can simplify error handling in subroutines, as well as callers of subroutines.
Default handling for untrapped conditions
In addition to FAILURE condition, you can raise other conditions. The format of the RAISE instruction is the same. Here we raise SYNTAX condition with an error number of 5 and a message Out of memory:
RAISE SYNTAX 5 DESCRIPTION "Out of memory"But there is one important difference here. Remember that the default handling for FAILURE, when not trapped, is to just carry on. So for example, if the caller doesn't trap FAILURE before he calls Reformat(), and you raise FAILURE condition, then nothing will happen as far as the caller is concerned. Your subroutine will still terminate at the RAISE instruction. But the caller will continue on, with whatever consequences occur. For example, the NewData variable will have never been assigned any reformatted data.
But the default handling for SYNTAX condition, when not trapped, is to end the script. So if you raise SYNTAX condition, and the caller isn't trapping it, then the caller terminates and REXX will display the error number and message you set. So you can use SYNTAX for "fatal" errors. (ie, The caller must explicitly trap SYNTAX or be terminated upon an error).
Note that HALT is like SYNTAX (ie, terminates the caller if not trapped). ERROR is like FAILURE in that, if the caller doesn't trap it, execution continues on as if nothing happened in the caller. This may have some consequences. Consider the following:
myret = MySub() RETURN MySub: RAISE ERROR 1 RETURNThe caller calls MySub, hoping to get back a value to assign to the variable "myret". But MySub raises the ERROR condition, so MySub terminates on that line. Control goes back to the caller. Because the caller has not trapped the ERROR condition (before calling MySub), then things proceed as normal. The return value of MySub is assigned to the variable "myret". But wait. What did MySub return? MySub did not return anything. It was ended on the RAISE instruction. So what does REXX do now? REXX raises a SYNTAX condition with category/sub-error 44.1 (ie, a function did not return a value when it needed to do so).
This brings up an important point. You don't have to worry about RAISE'ing the SYNTAX or HALT conditions because if the caller doesn't trap these, he will be abruptly terminated. But if you raise ERROR or FAILURE, and the caller is not trapping these, you should be aware that this could inadvertently raise a SYNTAX error if the caller tries to use some value he expects back from you. For this reason, the RAISE instruction also allows you to specify a RETURN keyword, and then some value you wish passed back. For example, here we raise the ERROR condition, and if the caller hasn't trapped that, we'll be returning the value "Some string" to him.
MySub: RAISE ERROR 1 RETURN "Some string" RETURNIf you want to return some expression, you should enclose it between parentheses. For example, here we return the value of MyVar minus 10.
MySub: RAISE ERROR 1 DESCRIPTION "An error occurred" RETURN (MyVar - 10) RETURN
Supplying the number or message via an expression
If you want to supply the error number, and/or the message, via an expression, for example, maybe from the value of some variable or the return string from a function, then you need to put parentheses around the expression. Below, I take the error number from the variable named MyError, and the message from the return of the UNIXERROR() function:
RAISE FAILURE (MyError) DESCRIPTION (UNIXERROR(MyError))
SYNTAX numbers/messages
If raising SYNTAX, then your numbers should follow the scheme described in SYNTAX condition. For example, if the caller doesn't supply the data to reformat, maybe you'd use a general category number of 40, and a specific number of 5 (ie, a required argument is missing). For a bad Operation, maybe you'd use a category number of 53, and no specific number (since there isn't a specific message that fits our needs). When supplying both a category and specific number, you separate them with a dot. For example, here we specify a category of 40 and sub-error of 5:
IF LENGTH(OriginalData) = 0 THEN RAISE SYNTAX 40.5 DESCRIPTION "No data supplied"If you want to use only a category number, then do not specify a dot followed by a sub-error number (or specify a suberror number of 0 after a dot). Furthermore, if you don't supply a description with SYNTAX, then the default message is chosen for you. For example, here we'll get the default message for category 53:
IF Operation \= "REMOVETAGS" & Operation \= "COMPRESS" THEN RAISE SYNTAX 53If you need to construct an error message for a specific SYNTAX error, then you shouldn't use the default message verbatim. Most of the sub-error messages have placeholders for which you need to supply some strings to substitute. (In Reginald, these placeholders look like %s, %r, %d or other patterns where the first character is a % followed by a single letter. In other interpreters, the placeholders may be full words enclosed between < and > such as <name> or <position>).
For example, consider the sub-error message for category 40, suberror 5. This message concerns a required argument that was omitted. Its format in Reginald is:
Missing argument in invocation of "%r"; argument %d is requiredYou need to supply the name of your subroutine in place of the %r, and the argument number that was missing in place of the %d. For example, suppose the caller omits the OriginalData argument to Reformat(). That is supposed to be the first arg, so it's arg number one. You therefore want your error message to read:
Missing argument in invocation of "Reformat"; argument 1 is requiredReginald's ERRORTEXT function can be used to obtain a particular. default error message related to SYNTAX. You pass the error and sub-error numbers, separated by a dot, and ERRORTEXT returns the message with its placeholders.
SAY ERRORTEXT("40.5")But Reginald allows you to pass additional arguments to substitute, and will return the desired error message with your arguments substituted for the placeholders. So for example, we need to pass our subroutine name first (since that comes first in the error message) and then our argument number, as so:
SAY ERRORTEXT("40.5", "Reformat", "1")Here then is our Reformat subroutine written to support standard SYNTAX messages:
Reformat: PROCEDURE OriginalData = ARG(1) Operation = TRANSLATE(ARG(2)) /* Check for an unsupported Operation */ IF Operation \= "REMOVETAGS" & Operation \= "COMPRESS" THEN RAISE SYNTAX 53 /* Check for the proper data format */ IF LENGTH(OriginalData) = 0 THEN RAISE SYNTAX 40.5 DESCRIPTION (ERRORTEXT("40.5", "Reformat", "1")) RETURN NewDataThe good thing about using the standard SYNTAX error messages is that, when Reginald's error message box pops open and the user clicks on the Help button, it will bring up an appropriate help page. For example, for that error 40.5, it will bring up a help page telling him that he didn't pass an argument that was required, and offering possible causes for, and solutions to, that problem. So for any error you may encounter for which there is an existing, appropriate SYNTAX message, you should raise SYNTAX with the appropriate category (and perhaps sub-error) number.
Here is a routine you can use to create an html page with a table showing all the category and sub-error messages (with placeholders) in your interpreter:
/* Creates a listing of SYNTAX error numbers/messages for the * REXX interpreter that runs this script, and creates a file * called "RexxErrors.htm" viewable with Internet Explorer. */ name = "RexxErrors.htm" MaxSubErrs = 127 Again: IF STREAM(name, 'C', 'OPEN BOTH REPLACE') == 'READY:' THEN DO SAY "Writing out" name || "..." PARSE VERSION rexxname level date date = STRIP(date) IF LEFT(rexxname, 5) == "REXX-" THEN PARSE VAR rexxname "REXX-" rexxname "_" version ELSE version = "Unknown" SIGNAL ON SYNTAX CALL LINEOUT name, "The following chart lists the ANSI error and sub-error messages associated with the SYNTAX condition for the" rexxname "REXX interpreter (version" version "released on" date || ").<P><TABLE NOBORDER>" DO i = 1 TO 100 msg = ERRORTEXT(i) IF msg \== "" THEN DO CALL LINEOUT name, '<TR><TD BGCOLOR="#77AAAA" VALIGN=TOP>' || i || '</TD><TD BGCOLOR="#77FFAA">' msg || '</TD>' DO p = 1 TO MaxSubErrs msg = ERRORTEXT(i || '.' || p) IF msg \== "" THEN CALL LINEOUT name, '<TR><TD BGCOLOR="#AAAA77" VALIGN=TOP> ' || i || '.' || p || '</TD><TD BGCOLOR=PINK>' || msg || '</TD></TR>' IF i = 4 THEN LEAVE p END END END END ELSE SAY STREAM(name, 'D') RETURN SYNTAX: /* Check if we simply tried to retrieve too many suberror messages */ IF SIGL == 20 THEN DO MaxSubErrs = MaxSubErrs - 1 SIGNAL Again END
USER condition
There is one more type of condition you can raise with the RAISE instruction -- the USER condition. Since REXX itself never raises this, you can use the USER condition as a unique condition (ie, you don't have to contend with REXX also using this condition for its own purposes). Furthermore, Reginald actually supports 250 USER conditions, numbered 1 to 250. You raise a USER condition very much like the other conditions, but the number is actually the USER number. For example, here is how you would raise the USER 1 condition:
/* Raise USER 1 with a message of "Hello there" */ RAISE USER 1 DESCRIPTION "Hello there"And of course, the caller can choose to trap USER 1 condition. He uses CATCH USER, but he must also specify the USER number. Here is how you would trap the USER 1 condition:
CATCH USER 1 /* Put the instructions here to handle * when USER 1 is raised. */In the handler for USER 1, you can use CONDITION('D') to retrieve the message, and CONDITION('C') reports the USER number. CONDITION('M') displays a message box with the error message.
CATCH USER 1 SAY "USER" CONDITION('C') "reports:" CONDITION('D')Of course, you can trap as many USER numbers as you would like. Here we trap USER 1 and USER 25:
CATCH USER 1 SAY "USER 1 occurred!" CATCH USER 25 SAY "USER 25 occurred!"With 250 different USER conditions, you have some flexibility to report different errors via this method. But you can also extend the range of error numbers reported. When you raise a particular USER condition, you can follow its number with a dot and then a particular error number, as so:
/* Raise USER 1 with an error number of 100 */ RAISE USER 1.100 DESCRIPTION "Hello there"Now CONDITION('C') will return the USER number (1 in the above example), and CONDITION('E') will return the error number (100).
It is also possible to trap all possible USER conditions by specifying a CATCH USER ANY. For example, here we trap any USER number being raised:
/* If any USER condition number is * raised, we jump here. */ CATCH USER ANY SAY "USER" CONDITION('C') "reports:" CONDITION('D')Individual CATCH USER statements you specify override the CATCH ANY handler. For example, here we specifically notate that we wish to CATCH USER 1 condition, but for all other USER numbers, we want to jump to the CATCH USER ANY:
/* Trap USER 1 condition specifically. So if it is * raised, we do this. */ CATCH USER 1 SAY "USER 1 occurred!" /* If any USER condition number is * raised (besides 1), we do this. */ CATCH USER ANY SAY "Some other USER condition occurred!"Note that USER conditions are like ERROR or FAILURE in that, if the caller hasn't trapped the particular USER number raised, then this doesn't abort the caller. The caller simply proceeds on with whatever consequences happen as a result of not trapping that USER number. So, you may wish to use the RETURN keyword to return some default value in this circumstance.