The latest groundbreaking isotope-powered concept of Ren-C is... the definitional error
But First, We Have To Define Failure...
Definitional errors now are an antiform state. The non-antiform state is currently called a WARNING!. So ERROR! is what you get if you UNLIFT a QUASI WARNING!
>> quasi 'null
== ~null~
>> unlift quasi 'null
== \~null~\ ; antiform
>> unlift quasi make warning! "foo"
** Error: foo
** Near: [*** make warning! "foo" **]
Or just ANTI it, which does the same thing.
Being an unstable antiform, you can't store errors in variables directly. But if you try to, it elevates the error state to an exception, e.g. it "panics".
>> var: anti make warning! "foo"
!! PANIC: foo
** Near: [*** make warning! "foo" **]
There's also a special behavior that they cannot occur in generic midstream evaluations, or they'll also be elevated to an exception:
>> (1 + 2 anti make warning! "foo" 3 + 4)
!! PANIC: foo
** Near: [*** make warning! "foo" ** 3 + 4]
So far this doesn't seem so profound. Rebol2 and Red can DO an ERROR! and raise an exception...which you also can't store in a variable or keep going in the middle of an expression.
BUT here's the twist:
>> lift anti make warning! "foo"
== ~#[warning! [
type: '
id: '
message: "foo"
near: [*** make warning! "foo" **]
file: '
line: 1
]]~
-
The ANTI did not trigger an irrecoverable error.
-
It created an antiform warning state, and then waited to see if something would LIFT it or not.
-
There was a LIFT and so it gave you back the QUASI WARNING! state
This is a crucial difference, as we will see. But first...
NOTE: I will be using the operation FAIL in the remaining text, instead of ANTI on WARNING! So approximately this:
fail: lambda [reason [text! block! error!]] [anti make warning! reason]
Let's Address the "Definitional" Part
What I mean when I say "definitional" is that there's a difference between these two cases:
bigtest: func [n] [
if n < 1020 [fail [n "is not big"]]
print [n "sure is a big number"]
]
definitional-bigtest: func [n] [
if n < 1020 [return fail [n "is not big"]]
print [n "sure is a big number"]
]
You may not appreciate the difference if you call them directly
>> bigtest 304
!! PANIC: 304 is not big
** Where: fail if bigtest args
** Near: [fail [n "is not big"] **]
>> definitional-bigtest 304
** Error: 304 is not big
** Where: fail if definitional-bigtest args
** Near: [return fail [n "is not big"] **]
But try using LIFT and you'll see they are different:
>> lift bigtest 304
!! PANIC: 304 is not big
** Where: raise if bigtest args
** Near: [raise [n "is not big"] **]
>> lift definitional-bigtest 304
== ~&[warning! [
type: '
id: '
message: "304 is not big"
near: [return raise [n "is not big"] **]
where: [raise if definitional-bigtest args]
file: '
line: 1
]]~
Functions can now choose to tell us when an error was something they knew about and engaged, vs. something incidental that could have come from any call beneath them in the stack.
Sound important? It should.
NOW BLAST SOME MUSIC FOR THIS WATERSHED MOMENT
I've added EXCEPT, which is an infix operation that reacts to failures...while THEN and ELSE just pass them on.
>> fail "foo" then [print "THEN"] else [print "ELSE"] except [print "EXCEPT"]
EXCEPT
As we saw in the beginning, if someone doesn't handle the failure it gets elevated to an exception eventually:
>> fail "foo" then [print "THEN"] else [print "ELSE"]
!! PANIC: foo
** Near: [raise "foo" ** then [print "THEN"] else [print "ELSE"]]
Remember the old, bad ATTEMPT? It would evaluate a block as usual but return NONE in the event there was an error encountered:
rebol2>> attempt [print "Attempting to read file" read %nonexistent-file.txt]
Attempting to read file
== none
rebol2>> attempt [print "Attempting but made typos" rread %nonexistent-file.txt]
== none
It was too dangerous to use. Note that it can't distinguish an error it could sensibly react to (e.g. an error coming from READ failing) from an error it cannot (in this case a typo of RREAD instead of READ, but it could be anything.)
With READ upgraded to turn its file-not-found error to be definitional, you can use TRY based on definitional errors for safety! (ATTEMPT is something non-error related, now...)
>> try (print "Attempting to read file" read %nonexistent-file.txt)
Attempting to read file
== ~null~ ; anti
>> try (print "Attempting but made typos" rread %nonexistent-file.txt)
Attempting but made typos
!! PANIC: rread word is attached to a context, but unassigned
** Near: [rread ** %nonexistent-file.txt]
(TRY will give you NULL in the event of an error, and can only react to the last evaluation in the expression. If you want to protect a series of operations in the style of old-ATTEMPT, you can use RESCUE... but you'll have to move capture of the return result inside the RESCUE block, since it returns either the un-antiformed error or null.)
It will take time for natives to be audited and have their random PANICs turned to be definitional-FAIL-when-applicable. Until then, most won't have errors that can be intercepted like this.
But other than that...
It's Here. It's Now. It's Committed!
Note That Non-Definitional Panic Exists...
If you want a divergent function that immediately go to an exception state, use PANIC.
That's clearer than calling FAIL with no RETURN, where a reader can't tell if it's going to be piped along and eventually RETURN'd or LIFT-ed somewhere.
return case [
... many pages of code ...
... [fail "Some error"] ; need to be able to say PANIC here if you meant that
... many pages of code ...
]
When writing a function and deciding if an error should be RETURN FAIL or PANIC, think about the use case. Do you feel that the call is fundamentally malformed (in the way a type checking error on a parameter would be thought of as a mistake), or did you understand what was asked clearly...but just couldn't do it?
It's subtle, but I think the pattern is emerging pretty clearly of when you should PANIC vs. RETURN FAIL.