7. Failure Types

The error handling tools we’ve seen so far have one serious shortcoming, there is no means for identifying the cause of the error. In the application, which we augmented with error handling in the previous tutorial, we don’t check at all what the cause of the failure is. Instead, we simply assumed that a failure value means invalid input was entered. Whilst this is the case in our simple application, it is not the case for more complex real world applications where there are many potential sources of errors.

7.1. Identifying the cause of Failures

Each failure value has an associated type, which is a value that identifies the cause of the failure. The failure type can be obtained using the fail-type meta-node from the core module. If the argument of fail-type evaluates to a failure, the meta-node returns its type, otherwise if the argument does not evaluate to a failure or evaluates to a failure without a type, the meta-node returns a failure.

The meta-node fail-type is a bit clunky to use as it, itself, returns a failure if the argument does not evaluate to a failure value. The utility fail-type? meta-node, also from the core module, takes two arguments, a value and a failure type, and returns true if the value evaluates to a failure of that type.

A value used as a failure type is generally bound to a constant node, which is used in place of the raw value. An accompanying node, with the same identifier but with a trailing ! is bound to a failure of the type.

7.2. Example: Checking Failure type in Adding Numbers Application

The type of the failure returned by to-int, when given a string that does not contain a valid integer, is designated by the node Invalid-Integer, from the core module. The node Invalid-Integer! is bound to a failure of type Invalid-Integer.

We can use the fail-type? meta-node to explicitly check whether the failure is of the type Invalid-Integer. Simply replace fails?(value) with fail-type?(value, Invalid-Integer) in the definition of the error-message meta-node.

Improved error-message Meta-Node. 

error-message(value) :
    case(
        fail-type?(value, Invalid-Integer) : "Not a valid number!",
        ""
    )

The new implementation returns the string “Not a valid number!” only for errors caused by invalid input being entered. It returns the empty string for errors of any other type.

7.3. Creating Failure Values

Failures are limited in use if they can only be created by builtin meta-nodes. You can create your own failure values using the fail meta-node, which takes one optional argument — the type of the failure. If the type argument is not provided, a failure without a type is created.

Example. 

# Creates failure with no type
fail()

# Creates a failure with type `My-Type`
fail(My-Type)

Example: Positive Numbers Only

Suppose for some reason, we’d like to limit the numbers being added, in the Adding Numbers application, to positive numbers. It could be that the numbers represent amounts for which negative values do not make sense in the context of the application.

Let’s write a meta-node, validate, which takes an integer value and returns that value if it is greater than or equal to zero. Otherwise it returns a failure of a user-defined type designated by the node Negative-Number.

Meta-Node validate

validate(x) :
    case(
        x >= 0 : x,
        fail(Negative-Number)
    )

If the argument x is greater than or equal to zero it is returned directly, otherwise a failure, created using the fail meta-node, of type designated by Negative-Number is returned.

Now let’s bind the Negative-Number node to a value, which uniquely identifies the failure. For now let’s choose the value -1. While we’re at it let’s also define the Negative-Number! meta-node which is simply bound to a failure of type Negative-Number.

Failure Type `Negative-Number `. 

Negative-Number  <- -1
Negative-Number! <- fail(Negative-Number)

We can simplify validate by substituting fail(Negative-Number) with Negative-Number!:

Simplified validate Meta-Node. 

validate(x) :
    case(
        x >= 0 : x,
        Negative-Number!
    )

[Note]

It does not matter whether you place the binding declarations of the nodes Negative-Number and Negative-Number! before or after the definition of validate.

To incorporate this in our application, we have to change the nodes, to which the input fields are bound, from a and b to input-a and input-b.

Replace a with input-a, in the text field for A, and b with input-b in the text field for B.

...
<label>A: <input value="<?@ to-int(input-a) ?>"/></label>
...
<label>B: <input value="<?@ to-int(input-b) ?>"/></label>
...

Also change the setting of initial values such that they are set on nodes input-a and input-b rather than a and b.

0 -> input-a
0 -> input-b

Now we’re going to bind a to the result of validate applied on input-a and we’re going to bind b to the result of validate applied on input-b.

a <- validate(input-a)
b <- validate(input-b)

Finally let’s update the error-message meta-node to return “Number must be greater than or equal to 0!” in the case that the failure is of type Negative-Number.

Updated error-message Meta-Node. 

error-message(value) :
    case(
        fail-type?(value, Invalid-Integer) :
            "Not a valid number!",
        fail-type?(value, Negative-Number) :
            "Number must be greater than or equal to 0!",
        ""
    )

Build and run the application and enter a positive number in one field and a negative number in the other:

A: 1, B: -1, Number must be greater than or equal to 0!, A + B = 1

The error message, explaining that a positive number (or zero) must be entered, is displayed next to the field where the negative number was entered, B in this case. The result of the addition with the new numbers entered is not displayed, instead the previous result is retained, as expected.

Change the negative number to an invalid number:

A: 1, B: foo, Not a valid number!, A + B = 1

The error message changes to “Not a valid number!” and the displayed sum is unchanged, as in the previous versions.

Now change the value to a valid positive number:

A: 1, B: 2, A + B = 3

The error message disappears and the new sum is displayed.

7.4. Proper Failure Types

There is one issue with the application we’ve just developed. There is no guarantee that the arbitrary constant -1 uniquely represents a failure of type Negative-Number. If all failure types used arbitrary integer constants, there is no guarantee that -1 doesn’t already represent a builtin failure type, such as Invalid-Integer. Whilst it so happened to work, it is certainly not robust, especially when bringing in third party libraries.

A value, which is guaranteed to be unique, can be obtained by taking a reference to the raw node object of Negative-Number.

A reference to the raw node object, of a node, can be obtained using the & special operator, which takes the identifier of the node as an argument. Raw node references are mostly useful when writing macros, which you’ll learn about in a later tutorial. For now all that you need to know is that this value can serve as the failure type, i.e. can be compared using =, and is guaranteed to be unique.

Replace the binding declaration for Negative-Number with the following:

Proper Negative-Number Failure Type. 

Negative-Number  <- &(Negative-Number)

And now we have a robust way of distinguishing between failures originating from to-int, due to the input fields not containing valid integers, and errors originating from our own application logic.