The Haxial Programming Language
Error Management

An error occurs when a command is unable to complete its action. For example, if you invoke a command that attempts to use a file of a certain name, but there is no file in existence with that name, then an error occurs. Or if you invoke a command that needed a number for some purpose, but you supplied it with a number bigger than the maximum it supports, then an error occurs.

Usually KL source code can be written without regard for errors, and it will still operate correctly or adequately. If an error occurs, whatever was being done is simply aborted, and an error message is displayed to the user. However in some situations you will need or want to insert some extra error management commands, and in some situations code should be modified to increase its level of error safety.

Error Objects

An error object contains information describing an error that occurred. An error object can be created using the MError.Create command. Usually you would only create an error object if you wanted to initiate an error. The command has this form:

MError.Create outRef, inType;

Error type numbers are used to identify different types of errors. To retrieve or modify the uint32 error type stored in an error object, use these commands:

MError.GetType inRef, outType;
MError.SetType inRef, inType;

When an error has been initiated by a host operating system, the error type/ID number from the host is stored in the error object (as sint32), and can be accessed using the following commands. The interpretation of this number depends on the particular host and is different for different types of hosts.

MError.GetHostErrID inRef, outID;
MError.SetHostErrID inRef, inID;

Some options can be set for an error. The MError.kOption.Fatal option means that the program should be terminated after the error is reported to the user. The MError.kOption.Silent option means that the error should not be reported to the user but should otherwise operate normally. To set and get the options, use these commands:

MError.GetOptions inRef, outOptions;
MError.SetOptions inRef, inOptions;

Error objects can store a uint32 "operation" number, and a uint32 "extra" number. These are unused. You can use them for your own purposes. Access them using these commands:

MError.GetOperation inRef, outOper;
MError.SetOperation inRef, inOper;
MError.GetExtraNumber inRef, outNum;
MError.SetExtraNumber inRef, inNum;

Error objects can store a short piece of "information" text. This can be used for various purposes. For example, it could be set to the name of a file that was being used when the error occurred. Access this text using these commands:

MError.GetInfoText inRef, dstInfoText;
MError.SetInfoText inRef, srcInfoText;
Responding to an Error

In the early days of software development, every command that could potentially result in an error had an output parameter or return value that indicated whether an error occurred, and perhaps also what type of error occurred. This was very tedious because it required that a correctly-written program check the returned error value (with an "if" statement) every time it invoked a command. An average-sized program may have had hundreds or thousands of invocations of commands, each requiring an explicit error check.

KL uses a better, almost effortless, error system. If a command is invoked, and an error occurs inside that command, then the command does not return. Instead execution is transferred to an "ErrorResponse" section. For example:

cmd Test;
    DoSomethingThatMightFail;
    DoSomethingElse;
    EatMoreNachos;

    ErrorResponse;
        StopEatingNachos;
    ContinueError;
ecmd;

In this example, if no errors occur, then the command "DoSomethingThatMightFail" and the 2 commands following it will be executed. The command "StopEatingNachos" will NOT be executed because it is intended to be executed only in the event of an error occurring.

If an error occurs inside "DoSomethingThatMightFail", then "DoSomethingElse" and "EatMoreNachos" are NOT executed. "DoSomethingThatMightFail" does not return, instead execution is transferred to the section beginning with "ErrorResponse" where "StopEatingNachos" is executed.

But what if the Command Implementation does not contain any "ErrorResponse" section? Consider this example with 2 Command Implementations, one invoking the other:

cmd Test;
    DoSomethingThatMightFail;
    DoSomethingElse;
    EatMoreNachos;
ecmd;

cmd Main;
    Test;

    ErrorResponse;
        StopEatingNachos;
    ContinueError;
ecmd;

In this example, the command "Main" invokes the command "Test", and the command "Test" then invokes the command "DoSomethingThatMightFail" and others.

If an error occurs inside "DoSomethingThatMightFail", then "DoSomethingElse" and "EatMoreNachos" are NOT executed. "DoSomethingThatMightFail" does not return, instead execution is transferred to the "ErrorResponse" section in the command that invoked "Test", that is the "ErrorResponse" section in the command "Main".

Thus when an error occurs, it looks for an ErrorResponse section in the same Command Implementation. If there is none there, it looks for an ErrorResponse section in the Command Implementation that invoked the current Command Implementation. If there is none there, then it looks in the next earlier Command Implementation, and so forth, progressing back through the chain of invoked Command Implementations until it finds one with an ErrorResponse section. Then execution is resumed at the beginning of the first found ErrorResponse section.

If no ErrorResponse section is found in any of the Command Implementations that have been invoked, then execution is transferred to a hidden base ErrorResponse section that always exists. This ErrorResponse section performs a default action of displaying an error message to the user.

You may have noticed that the ErrorResponse sections end with a "ContinueError" command. This command cannot be omitted. To understand what this does, consider this example with multiple ErrorResponse sections:

cmd Test;
    DoSomethingThatMightFail;
    DoSomethingElse;
    EatMoreNachos;

    ErrorResponse;
        StopEatingNachos;
    ContinueError;
ecmd;

cmd Main;
    Test;
    InitiateGabFest;

    ErrorResponse;
        FreakOut;
    ContinueError;
ecmd;

In this example, if an error occurs inside "DoSomethingThatMightFail", then "DoSomethingElse" and "EatMoreNachos" are NOT executed. "DoSomethingThatMightFail" does not return, instead execution is transferred to the section beginning with "ErrorResponse" within the Command Implementation "Test".

There the command "StopEatingNachos" is executed, followed by the "ContinueError" command. "ContinueError" causes execution to jump to the "ErrorResponse" section within the Command Implementation "Main", where "FreakOut" is executed. The command "InitiateGabFest" is NOT executed. Here is the example again, this time with comments:

cmd Test;
    DoSomethingThatMightFail; {ASSUME THIS ERRORS}
    DoSomethingElse; {NOT EXECUTED}
    EatMoreNachos;   {NOT EXECUTED}

    ErrorResponse;
        StopEatingNachos; {EXECUTED}
    ContinueError; {JUMPS TO ErrorResponse IN Main}
ecmd;

cmd Main;
    Test; {EXECUTED, PARTIALLY}
    InitiateGabFest; {NOT EXECUTED}

    ErrorResponse;
        FreakOut; {EXECUTED}
    ContinueError;
ecmd;

Note that in this example, "Test" is only ever invoked by "Main", but in a real-world program, "Test" might be invoked by multiple different Command Implementations, and in that case the "ContinueError" within "Test" jumps to whichever Command Implementation invoked "Test" this time that "Test" was invoked (and that can be different each time that "Test" is invoked).

Thus the "ContinueError" command causes execution to jump to the next ErrorResponse section (in the invoker, or in the invoker's invoker, or wherever the next one is found). If there are no more ErrorResponse sections, then it jumps to the hidden base ErrorResponse section that always exists.

But what does the "ContinueError" in the hidden base ErrorResponse section do? Where does that jump to?! Well, the truth is, the base uses the "CancelError" command instead of "ContinueError", and you can do that too, anytime you want. An "ErrorResponse" section must end with either "ContinueError" or "CancelError". It cannot end with nothing.

"CancelError" causes the current Command Implementation to immediately return/finish. Unlike "ContinueError", it does not jump to the next "ErrorResponse" section. For example:

cmd Test;
    DoSomethingThatMightFail; {ASSUME THIS ERRORS}
    DoSomethingElse; {NOT EXECUTED}
    EatMoreNachos;   {NOT EXECUTED}

    ErrorResponse;
        StopEatingNachos; {EXECUTED}
    CancelError; {Note the Cancel here}
ecmd;

cmd Main;
    Test; {EXECUTED, PARTIALLY}
    InitiateGabFest; {NOW THIS IS EXECUTED}

    ErrorResponse;
        FreakOut; {NOT EXECUTED}
    ContinueError; {NOT EXECUTED}
ecmd;

As a matter of fact, you can also use "ContinueError" and "CancelError" commands WITHIN your ErrorResponse section, not only at the end. This does not change the requirement that the ErrorResponse section must end with either "ContinueError" or "CancelError", rather it simply means you can use them additionally. For example:

cmd Test;
    {...do some things that might fail...}

    ErrorResponse errType;
        if errType == MError.kError.MaxLimit;
            {...perform alternate action...}
            CancelError;
        eif;
    ContinueError;
ecmd;

Both "ContinueError" and "CancelError" have an immediate effect, meaning if you use either of them within your ErrorResponse section, then the remainder of your ErrorResponse section is not executed because they cause execution to jump away (to the next ErrorResponse section in the case of "ContinueError", or simply returning in the case of "CancelError"). For example:

cmd Test;
    {...do some things that might fail...}

    ErrorResponse errType;
        if errType == MError.kError.MaxLimit;
            {...perform alternate action...}
            CancelError;
            InitiateGabFest; {NEVER EXECUTED}
        eif;
        ContinueError;
        FreakOut; {NEVER EXECUTED}
    ContinueError;
ecmd;

You might have noticed that in the last 2 examples, the "ErrorResponse" command has a parameter value "errType". This parameter is optional. If supplied, it is the name of a new variable to create and set to the error type number of the error that occurred. Note it is NOT the name of an existing variable to set. Rather it defines a new variable like how the "var" command defines a new variable.

If you want access to all the error information, then ErrorResponse has a second optional parameter, the name of a new variable to create and set to a reference to the error object that contains all the error information. If you want the second parameter (the object) but not the first parameter (the type number) then supply "null" for the first parameter. Example:

cmd Test;
    {...do some things that might fail...}

    ErrorResponse null, errInfo;
        var errOper, uint32;
        MError.GetOperation errInfo, errOper;
        {...do something with errOper...}
    ContinueError;
ecmd;

The above example is merely querying the error object using "MError.GetOperation", but if you used "MError.SetOperation" instead, that is if you modified the error object, then it becomes the new error information that "ContinueError" will use and supply to the next ErrorResponse section. You might change the error object to add more information describing the error, or to change the error type from a generic type to a more specific type.

If you are an extra clever little doggy, you may have wondered what happens if an error occurs within an ErrorResponse section. It is treated as if the Command Implementation containing that ErrorResponse section had no such ErrorResponse section at all. This can also be thought of as causing automatic continuance of the error, with the new error information overwriting the original. The currently-executing ErrorResponse section is NOT re-entered because that could easily result in an infinite loop.

Only 1 ErrorResponse section per Command Implementation is permitted, and it must be at the end. The ErrorResponse section can use any of the variables defined in the Command Implementation including ones defined above (outside) the ErrorResponse section.

The Goto Command

Using the Goto command to escape the ErrorResponse section is permitted. It effectively cancels the error. It can be used to retry an action that failed. Example:

cmd Test;
    loc tryAgain; {destination for goto}
    DoTroublingThings;

    ErrorResponse;
        var retryCount, uint, 0;
        if retryCount != 5;
            inc retryCount; {add 1 to retryCount}
            goto tryAgain;
        eif;
    ContinueError;
ecmd;
Finally

The "Finally" command can be used to cause some commands to be executed both in the event of an error occurring and also in the event of no error occurring. In the following example, assume that no error occurs:

cmd Test;
    DoSomethingThatMightFail; {EXECUTED}
    DoSomethingElse; {EXECUTED}
    EatMoreNachos; {EXECUTED}

    Finally;
    CloseMyFiles; {EXECUTED}
ecmd;

Here is the same example again, but this time assume that the "DoSomethingThatMightFail" command causes an error.

cmd Test;
    DoSomethingThatMightFail; {EXECUTED, PARTIALLY}
    DoSomethingElse; {NOT EXECUTED}
    EatMoreNachos; {NOT EXECUTED}

    Finally;
    CloseMyFiles; {EXECUTED}
ecmd;

If the Command Implementation uses "ErrorResponse", then "Finally" cannot be used. They are mutually-exclusive.

If an error occurs within the "Finally" section, and the "Finally" section was entered as a result of an error, then the behavior is the same as an error occuring within an "ErrorResponse" section. If the "Finally" section was entered normally without any error, then the behavior of an error is the same as an error occurring outside of any "ErrorResponse" or "Finally" section.

Error Safety

A command is said to be error-safe if runtime failures within the command will not produce bad effects, such as garbage values being stored, invalid output, memory leaks, or crashes. There are 5 levels of error safety:

  1. Never fails -- The command is guaranteed to succeed in all situations. This is the best level of error safety.
  2. Strong safety -- Commit or rollback semantics. The command can fail, but if it does fail, it is guaranteed to have no observable side effects. In the event of a failure/error, the command "rolls back" or restores everything to the state it was in at the beginning of the command, making it seem as if the command was never executed. Alternatively, it delays modification of persistent objects (commitment) until it is certain that the modification will succeed and there will be no possible errors after that.
  3. Basic safety -- The command can fail and it can have side effects. However any stored data will contain valid values. For example, it might partially modify an object, and then an error occurs, and it leaves the object in the partially modified state, and although the modification is unfinished, the state of the object is still a valid state (the object is changed but not damaged in the event of failure).
  4. Weak safety -- If the command fails, invalid data may be stored, but the command does not cause a crash.
  5. No safety -- No guarantees are made. In the event of failure, may crash or cause a subsequent crash. This is the worst level of error safety.

The best level of safety, never failing, is frequently difficult or impossible to implement. Usually basic safety is the minimum acceptable level, but obviously strong safety (commit or rollback) is preferred. Sometimes it is impossible to rollback a modification, or the rollback itself can fail.

After you have finished writing a Command Implementation, it is a good idea to look at it and determine what its level of error safety is. You may discover that you need to make a minor modification or two to change it from weak safety to basic safety, for example. Sometimes simply reordering the steps in a Command Implementation can upgrade it to the next level of safety.

For example, consider a situation where you have 3 persistent variables, a reference to a text object, a reference to an array object, and a boolean indicating whether initialization has been done. Initialization requires that you create the 2 objects. Here is your first attempt at writing your initialization command:

set info.initialized, true;
MText.Create info.txo;
MNumArray.Create info.ary, uint32;

This code has weak safety because the MText.Create command or the MNumArray.Create command may fail (frex because of insufficient free memory), leaving the "info.initialized" variable incorrectly/invalidly set to true. However consider what happens if you simply reorder to the end the setting of the boolean:

MText.Create info.txo;
MNumArray.Create info.ary, uint32;
set info.initialized, true;

Now "info.initialized" will ONLY be set to true if the creation of the 2 objects succeeded, upgrading your Command Implementation from weak safety to basic safety.

It is not strong safety because the MText.Create command may succeed and then the MNumArray.Create command fails, and thus the "info" record is valid but modified (an observable side-effect). If you decided that strong safety was necessary in this situation, then you could change the code as follows:

var txoTemp, RText, null;
MText.Create txoTemp;
var aryTemp, RNumArray, null;
MNumArray.Create aryTemp, uint32;
set info.txo, txoTemp;
set info.ary, aryTemp;
set info.initialized, true;

The code now has strong safety. None of the "set" commands can fail (except if "info" is a null reference but then it does not matter what happens because there is no "info" record to protect anyway). In this example, the strong safety has been achieved by using commit semantics, meaning we delayed all modification of the "info" record until we were sure that no more errors could occur.

Destruction of objects is managed automatically, so there is no problem if MText.Create succeeds but then MNumArray.Create fails. The destruction of the created-but-unused text object will occur automatically. If we were writing this in a programming language that required manual destruction of objects by the programmer (such as the C language), then it would be more difficult to achieve strong safety in this example.

Initiating an Error

You can initiate an error using the MError.Fail command, for example:

MError.Fail MText.kError.MaxLimit;

The above supplies only an error type number. If you want to supply more information, you can create an error object, and then invoke MError.Fail with it:

var errInfo, RError, null;
MError.Create errInfo, MText.kError.MaxLimit;
MError.SetOperation errInfo, 666;
MError.SetInfoText errInfo, "Less talk, more action.";
MError.SetOptions errInfo, MError.kOption.Fatal;
MError.Fail errInfo;

If there is an "ErrorResponse" section in the Command Implementation where you invoke MError.Fail, then MError.Fail will cause execution to jump to that "ErrorResponse" section. Otherwise to the next "ErrorResponse" section.

There is also a convenient "FailIf" command, frex:

MError.FailIf requestedSize > 1000, MText.kError.MaxLimit;

Equivalent to:

if requestedSize > 1000;
    MError.Fail MText.kError.MaxLimit;
eif;
Format of Error Type Numbers

An error type number is a uint32 value with 2 halves. The upper 16 bits contain a number identifying the module where the error occurred or the module that is directly related to the error. The lower 16 bits contain a number identifying the specific type of error. When converted to text for display, the 32-bit error type number is displayed as 2 numbers (module number first) with a forward slash character between.

Program-specific error type numbers can be defined. In this case, the module part is set to a module number named "Program" (use the MError.kModule.Program constant, it is already shifted up 16 bits to place it in the upper half). In the lower 16 bits you can store any number but it should be no less than 128 because numbers 0-127 are reserved for generic error types. Here is an example of defining your own program-specific error types and then initiating an error:

enum kMyError, uint32;
    val BadSeafood,         MError.kModule.Program | 128;
    val ExplosiveGas,       MError.kModule.Program | 129;
    val InsufficientFlavor, MError.kModule.Program | 130;
eenum;

MError.Fail kMyError.BadSeafood;