Why Aren't THROW and CATCH Used For Errors?

While many languages speak of "throwing" and "catching" errors, this isn't how Rebol uses the terms.

Instead, 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: eval func [] [
    if condition [return result]
    some code
    case [
        condition [return code]
        condition [more code, return result]
    ]
    additional code
    return result
]

You can, if you like, THROW an error... plain or raised...and CATCH it. But that's just because you can throw anything. Packs are fine, too:

>> [a b]: catch [
       case [
           1 = 2 [throw pack [10 20]]
           1 = 1 [throw pack [100 200]]
       ]
   ]
== 100

>> a
== 100

>> b 
== 200

So it's really about throwing whatever you like--not specific to errors or error handling.

THROW and CATCH are a great lightweight feature for control flow, that people really should be using more often than they do. (Ren-C uses "definitional throw", which means there's no risk of you calling a routine that would accidentally catch a throw that wasn't meant for it--which is quite important!)

Definitional Errors Use RAISE+TRAP

If you're dealing with definitional errors, then you RAISE them and then TRAP them.

>> trap [raise 'foo]
== make error! [
    type: ~null~
    id: 'foo
    message: ~null~
    near: '[raise 'foo **]
    where: '[raise entrap trap eval catch* enrescue eval rescue console]
    file: ~null~
    line: 1
]

You can also use EXCEPT to trap with an infix construct:

>> (raise 'foo) except e -> [print e.id]
== foo

But by design, definitional errors must be triaged and handled immediately when they are returned--or the antiform error will "decay" into an "abrupt failure".

Abrupt Failures use FAIL+RESCUE

Generally speaking, it's not a good idea to react to abrupt failures (unless you are something like the CONSOLE, where all you are doing is reporting that the error happened.)

As mentioned above, a raised error will decay to an abrupt failure if it isn't triaged. But you can also cause an abrupt failure using FAIL.

(Fun tidbit: FAIL is implemented by raising a definitional error and then not triaging it before passing it on. Right now it passes it to NULL?, which doesn't use a meta-aware parameter convention so it forces decay to abrupt failure. fail: cascade [get $raise, get $null?])

To help emphasize that you should generally not be reaching for the RESCUE routine to recover from abrupt failures, it lives in sys.util.

 >> sys.util/rescue [
       foo: func [argument] [
           return argment + 20  ; whoops, typo
       ]
       foo 1000
    ]
== make error! [
    type: 'Script
    id: 'unassigned-attach
    message: '[:arg1 "word is attached to a context, but unassigned"]
    near: '[
        return argment ** + 20]
    where: '[foo enrescue eval rescue eval catch* enrescue eval rescue console]
    file: ~null~
    line: 2
    arg1: 'argment
]

Hopefully it's clear to anyone--upon light reflection--why thinking you can handle abrupt failures is generally misguided!

2 Likes