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.
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.
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.
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)
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! )
It does not matter whether you place the binding declarations of
the nodes |
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:
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:
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:
The error message disappears and the new sum is displayed.
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.