Rebol2 had a concept which had the inklings of definitional errors, in the sense that if you would RETURN an ERROR! value, it would escalate itself to an exception if not triaged with special functions at the callsite:
rebol2>> boom: func [] [return make error! "Boom"]
rebol2>> boom
** User Error: Boom
** Near: return make error! "Boom"
rebol2>> type? boom
== error!
rebol2>> (boom) print "No triage, this won't print"
** User Error: Boom
** Near: return make error! "Boom"
rebol2>> (disarm boom) print "Triaged, so this will print"
Triaged, so this will print
rebol2>> disarm boom
rebol2>> probe disarm boom
make object! [
code: 800
type: 'user
id: 'message
arg1: "Boom"
arg2: none
arg3: none
near: [return make error! "Boom"]
where: none
]
Why Was This Abandoned?
"Prior versions of REBOL used 'hot' errors. That is, you had to treat error values in a special way or they would automatically trigger error processing. This behavior was originally implemented to keep errors from propagating too far from their origins (the principle was to preserve as much as possible the locality of the error)."
"These hot errors turned out to be overkill, and the benefit of error locality was offset by the difficulty of handling error values in general. (See the articles of Ladislav Mecir who wrote excellent notes on this subject). It could be quite tricky at times."
I'm not sure what articles he's referring to from Ladislav. The link he provides has an error section where Ladislav points out some bugs and says "any Rebol value should be obtainable as a result of an expression, a result of a parenthesized expression, a result of a block evaluation and a result of a function evaluation", which seems pretty uncontroversial.
So... What Was The Problem?
One big ergonomic problem is that TRY intercepted errors, but didn't disarm them.
rebol2>> caught: try [boom]
** User Error: Boom
** Near: return make error! "Boom"
rebol2>> caught/id
** User Error: Boom
** Near: return make error! "Boom"
That's incredibly inconvenient. So why didn't TRY do a DISARM?
The problem is "hotness" was a property of all values of ERROR! type, and when you disarm them they'd become OBJECT!. If TRY were to DISARM the error it wouldn't be an ERROR! anymore, so you couldn't test to see if your expression had produced an error or not.
You get this because you're trying to pack too much information into one return result. It could have been addressed by having you write your TRY as arity-2 with an EXCEPT.
rebol2>> try-except: func [code handler /local e] [
return either error? e: try code [handler disarm :e] [:e]
]
rebol2>> try-except [boom] func [e] [probe e/arg1]
"Boom"
rebol2>> try-except [1 + 2] func [e] [probe e/arg1]
== 3
Ren-C gives you this with infix, and you can use an arrow function, so it's more pleasing:
boom except e -> [probe e.arg1]
Hot Errors Would Have Been Only Half The Story
It seems Rebol2 was on the right track by having a state you could carry in a function's return result, that would promote to a divergent panic if not triaged at the callsite.
But for it to gel, you need to draw a sharp line between divergent panics and ERROR!, and not mix up their interception (in fact, you should practically never intercept divergent panics).
Rebol2's TRY was a one-size-fits-none construct, considering any typo or deeply nested errors to be on par with one that was "RETURN'd" directly from the function you were calling:
rebol2>> probe disarm try [read %nonexistent.txt]
make object! [
code: 500
type: 'access
id: 'cannot-open
arg1: "/C/Projects/rebol2/nonexistent.txt"
arg2: none
arg3: none
near: [read %nonexistent.txt]
where: 'halt-view
]
rebol2>> probe disarm try [rread %nonexistent.txt]
make object! [
code: 300
type: 'script
id: 'no-value
arg1: 'rread
arg2: none
arg3: none
near: [rread %nonexistent.txt]
where: 'halt-view
]
This makes it nigh impossible to act upon the information reliably.
The Wayward Drift To All-Errors-Are-Exceptions...
Despite Rebol2 being on the cusp of meaningful error handling, R3-Alpha went the way of the exception fallacy. Jumping across arbitrary levels of stack running arbitrary code in order to handle an error in stack levels above is something that the software industry has pretty much debunked.
For the most part, exceptions need to be reserved for things that should basically never happen. The rare systems that do handle them should be when it's required for mitigating damage or corruption that might occur if cleanup code doesn't get run. Exceptions are not what you want to use to deliver garden variety errors...you need direct contracts between caller and callee.