Should THROW and CATCH Be For Errors?

The historical Rebolism of THROW and CATCH has nothing to do with error handling.

It's a generic way to move values up the stack. It gives you a handy "out" from control flow:

result: catch [
    if condition [throw result]
    some code
    case [
        condition [more code]
        condition [more code, throw result]
    ]
    additional code
    throw result
]

The implementation of the feature is lightweight, and built on the same mechanic as RETURN. You could in fact use return to do this:

result: do func [] [
    if condition [return result]
    some code
    case [
        condition [return code]
        condition [more code, return result]
    ]
    additional code
    return result
]

But that's more heavyweight, because it gets binding involved (although I've suggested that maybe CATCH and THROW should be definitional, and the "heaviness" is a feature... not a bug).

It's A Neat Feature, But I Want THROW and CATCH For Errors

Error handling is undergoing a renaissance in Ren-C, and it's becoming completely pervasive.

Despite its glory, it is currently tied in with the junky names RAISE and EXCEPT.

case [
   ...code that may fail...
] then [
   ...stuff to do if non-NULL, non-VOID, non-RAISED error!...
] else [
   ... stuff to do if NULL or VOID or RAISED error!...
] except e -> [
  ... error handling ...
]

The word EXCEPT is bad for several reasons:

  • It implies "exception" handling, which definitional errors are specifically not...they can only be caught one stack step at a time.

  • It's not really a "verb"

  • It often comes right after an ELSE, and I don't like the alliteration of E and E

  • Almost every other language uses CATCH there

And RAISE isn't exactly my favorite word either. return throw e vs. return raise e also has that alliteration issue.

So What Can The Old THROW and CATCH Be Called?

It's a lot less important than the error handling application, but I still use the old construct.

You can actually do it with CYCLE and STOP:

result: cycle [
    if condition [throw result]
    some code
    case [
        condition [more code]
        condition [more code, stop result]
    ]
    additional code
    stop result
]

Being a looping construct, it has the side effect that it will keep repeating unless a value is emitted.

On the plus side...this helps resolve the semantic question of "what should a CATCH return when there's no THROW?" If what you have is CYCLE then your answer is that it keeps going. So you either put a FAIL at the bottom, a STOP at the bottom, or accept it will keep going.

On the minus side...if you use this inside a looping construct you'll be redefining BREAK, which historical CATCH would not do. (Similarly, if you use FUNC and RETURN to do this you'd redefine RETURN.)

With definitional BREAK and RETURN there'd be workarounds for those cases--you'd simply give another name to the outer break and return if you needed them. But that's not how things work today.

While CYCLE is Not Perfect, It Would Work For The Moment

We can meditate on what the ultimate answer would be. But there's only a few cases, and CYCLE will do.

Errors need THROW and CATCH...they're now too fundamental to not use the good words for.

We now have the word TRAP for those who don't want to use enfix, so these are equivalent:

if e: trap [some code] [
    handle e
]

(some code) catch e -> [
   handle e
]

But you get some real benefits from the THEN + ELSE + CATCH, and I think the importance of these enfix constructs will become apparent to those who aren't on the bandwagon (yet).

2 Likes

Looking over the instances, I'm finding that I've used the old meaning of CATCH and THROW more than I thought I did.

Not only that... I wound up feeling like I should probably use it more. It's a good thing, and there are a lot of situations with control structures that it simplifies. Because most languages don't give you this casual a construct for moving values up the stack, it's not something one thinks to reach for even if it's a good solution.

Beyond that, I'm working on a methodization of THEN and ELSE which should be able to turn several cases that would need an "EXCEPT" (or CATCH-AS-EXCEPT) into something that doesn't:

Applications of Isotopic Objects - #2 by hostilefork

So what I had feared of seeing a lot of parse data rules except e -> [...] is going to be able to be the normal ELSE, but with a parameter with error information for the parse:

parse data rules else e -> [...]

I can't be 100% positive that I'm not going to find a fatal flaw in it. But right now it looks pretty good.

It's making me wonder if dealing with RAISE and EXCEPT could be quarantined enough that you don't see them much. If so, it could very well twist the balance back to the traditional meaning of CATCH and THROW.

But...what about BUT ?

case [
   ...code that may fail...
] then [
    ..stuff to do if non-NULL, non-VOID, non-RAISED error!...
] else [
    ... stuff to do if NULL or VOID or RAISED error!...
] but e -> [
   ... error handling ...
]

It would be a little more puzzling for first time readers, yet I think if you're coming to programming anew you would not think it unnatural.

For now I'm going to stick with RAISE and EXCEPT but let's keep an eye on those terms and see if something better is possible.

2 Likes