Should RESCUE and CATCH return null if no fails/throws?

R3-Alpha's TRY added a refinement /EXCEPT for passing in a block or function to act as a handler in the case of an error being raised.

RESCUE was created to be a seemingly better name than "TRY". It appeared to have more parity with CATCH, and RESCUE:WITH paralleled CATCH:WITH.

I think the name is an improvement. And it paved the way for the short word TRY to fill another important role.

But there's a pattern in both of these constructs which is that they return a result whether something is caught or rescued or not.

>> catch [10 + 20]
== 30

>> warning? rescue [make warning! "this is *not* a rescued error"]
== \~okay~\  ; antiform

That particular behavior of rescue is particularly tricky because many cases check if a result is an ERROR? and use that as a detection of if a FAIL ran...and here we see that's not actually happening.

But I've been questioning the value of this mixing-up-of-return-results. If you want to get a value out of the block, why not do that by setting a variable? You're usually trying to set a variable anyway, e.g. value: rescue [...], what's wrong with moving it into the code?

 rescue [
     value: some-calculation-that-may-fail ...
 ] then warning -> [
     ... code to handle the error ...
 ] else [
     ... stuff to do if there was no error ...
     ... assume value is good ...
 ]

This cleanly separates out the code paths, allowing usage of null-sensitive constructs. So it means getting rid of the :WITH refinement on RESCUE and CATCH, instead using normal THEN/ELSE/etc. constructs with them:

>> catch [10 + 20]
== \~null~\  ; antiform

>> rescue [make warning! "not an antiform error"]
== \~null~\  ; antiform

In the case of CATCH, you can always just throw your final result, to get it to conflate with an ordinary throw (this is inexpensive.)

RESCUE can't do that (since it would only be able to return errors). Though you could piggy-back on CATCH if you really wanted to avoid variable declaration with a RESCUE...just throw your result:

 catch [rescue [... throw result] then e -> [e]]

I'm not opposed to the idea of code-golf-friendly constructs which could go ahead and do this squashing of results together. (CATCH-DO, RESCUE-DO?) But the clean expression with only returning the caught or trapped thing--and null otherwise--seems quite appealing to me for the primitive building block.

Also note: the existence of ENRESCUE

I made ENRESCUE to address the problem of distinguishing errors from other values, by returning the lfited form of the value in the case of no errors. So if your result is QUOTED! or QUASIFORM!, then you know it succeeded and all you have to do is UNLIFT it to get the result. WARNING! is the only plain value returned...so it is an unambiguous signal of a rescued failure.

>> null
== \~null~\  ; antiform

>> enrescue [null]
== ~null~

>> enrescue [10 + 20]
== '30

>> warning? enrescue [1 / 0]
== \~okay~\  ; anti  (it's just a plain WARNING! value, not quoted or quasi)

>> quoted? enrescue [make warning! "abc"]
== \~okay~\  ; antiform  (quoted warning, since it wasn't FAIL'd)

I don't know how that fits into the naming and scheme of things, but mentioning it.

I am not sure I like this. Being able to use then/else is great, but I don't particularly like having to move assignments into the code.

var: rescue [...]

Makes it clear, that this is an assignment, which, by the way, may error so I'm handling this.

rescue [var: ...]

looks like it is mostly error handling code, and the assignment is easily overlooked.

Compare:

if warning? data: rescue [	              
    inflate:max data uncompressed-size	           
][
    info "^- -> failed [deflate]^/"
    throw blank
]

With:

rescue [	              
    data: inflate/max data uncompressed-size	           
] then [
    info "^- -> failed [deflate]^/"
    throw blank
]

What makes the assignment clearer? I think the second case, in particular because it doesn't conflate data as a variable which "may hold a warning, or may hold the data". If you wanted to be "clear" you'd have to call it data-or-warning, which is too wordy.

It seems to me the second way puts the data: closer to what's being assigned to it, instead of separating it in an awkward and artificial way. The first case doesn't make it that obvious that inflate:max returns a value at all--maybe only RESCUE does? The only hint you have that you're not just capturing an error is the misleading name "data". If you mix up the data with the error that means you're going to need a test, and if warning? data: rescue is a lot of noise to see through.

(And that's a simple case that doesn't mention the specific error when it probably should. You could use ATTEMPT ... ELSE for this, which would just give you a null if it was an error otherwise the value. But I worry about today's ATTEMPT because it can make typographical errors or other changes hard to find, so it seems it should be improved to at least not scuttle some common errors of words not being bound.)

More complicated examples which are rescuing a section of code that doesn't just do a single assignment are even better.

What makes me feel better about it as a primitive is that it prevents mistakes in generic code like:

if warning? item: rescue [
    someone-elses-array: get-array-may-fail x y z
    pick someone-elses-array index
][
     ; may be a WARNING! value that just was in someone-elses-array
]

Again--I don't object to there existing some construct that conflates raised errors with plain error values, and has all the concerns which go with that. But it seems the best way is to give a solid routine with no holes in it to build on. Then let people do what they like with that.

Being able to get rid of RESCUE:WITH is clean, and you can also use use ELSE to provide clauses easily for the non-erroring case. I think it's an improvement.

1 Like

It seems, that my comment wasn't completely thought through.
You have convinced me.

2 Likes

Looks like we may get the best of both worlds now, with multiple returns! The RESCUE and CATCH could offer a second output which is the value that "falls out" of the evaluation.

>> [error value]: rescue [1 + 1]
== ~null~  ; anti

>> error
== ~null~  ; anti

>> value
== 2

>> [_ value]: rescue [1 + 1] then [print "error!"]

>> value
== 2

>> [_ value]: rescue [1 / 0] then [print "error!"]
error!
1 Like

Throwing Multi-Returns Is Very Useful

So there's a competing application for multi-returns, which is the ability to throw a multi-return... which is more useful:

>> [x fallthrough?]: catch [
       if false [throw pack [1 null]]
       throw pack [2 okay]
   ]
== 2

>> x
== 2

>> fallthrough?
== \~okay~\  ; antiform

This seems like a much better use of the multi-return ability.

2 Likes

So CATCH is fundamentally different from RESCUE here, in that RESCUE is trapping error antiforms that are sort of arising out of the blue and very well may not happen most of the time. But CATCH's raison 'd-etre is to synthesize a value up out of a block.

The argument "why don't you just put a THROW at the bottom" could be equally spun around to say that if you don't want the value to fall out, why not put the value that you do want to fall out as the last line of the CATCH?

In practice, I think that a CATCH that returns the block product when there's no throw is more generally useful than a CATCH that arbitrarily decides to give back NULL if you had no throw. Why not TRASH! ? I think asking the question "did you throw" is rarely what's of interest... and when it does come up, making null the last evaluation in the block (or throw null if you feel it's more obvious) likely works fine.

This competition doesn't apply to RESCUE.

There's no particular reason why RESCUE couldn't do a multi-return with the block's synthesized result as the second value.

1 Like

Well, yes there is... :frowning:

Because if the non-erroring case isn't returned as a "light" null antiform, you won't be able to signal ELSE.

A PACK! that has null in its first slot and something else in its second slot is not considered "nothing" in the way that an unboxed null antiform is.

"I'm Returning Nothing (Oh, But Here's What I Return)"

So this problem is kind of a general pattern where you ask for something... NULL is the signal of "no I couldn't do that"... and you still try to tunnel more information back in despite saying you returned nothing.

The unstable PACK! antiform is the escape mechanism that lets you say "I'm returning something, and it's nothing". That's the loophole that powers ELSE while still letting you return something that is equivalent to null in most circumstances (the "isotope" concept at work... "it's hydrogen to most people, but if you have a special sensor...")

But if you use the escape, you're returning something. No THEN/ELSE...which RESCUE needs.

Is ENRESCUE The Best We Can Do?

It's definitely not the best name. It originated when this was the function of TRAP, and ENTRAP seemed like a "enclosed trap" worked as a name. But with RESCUE, ENRESCUE lacks that ring.

It could be RESCUE:LIFT ... but that doesn't quite convey what's going on. It's more of RESCUE:MULTIPLEX or something of that ilk.

I'm not sure if my term "DUAL" should be used here, because that's coming to mean something fairly specific pertaining to the fundamental representation of variables.

It doesn't come up as much because if you want a dual overlay of an error with other things, you can just use a ^META assignment.

 try ^var: eval code
 if error? ^var [...]

So it may be a solution in search of a problem.

Long story short, RESCUE is not a multi-return function

1 Like

Thinking about the same problem with SYS.UTIL/RECOVER, I realized it's kind of a matter of phrasing... in that if your construct is phrased in the affirmative (e.g. SANDBOX), you can use EXCEPT.

So if instead of calling it RESCUE it was called something else, like... I dunno, for the sake of argument let's just call it SEQUENTIAL:

value: sequential [
    expr1
    expr2
    expr3
] except e -> [
    ; one of those expressions returned an ERROR!
]

If you haven't realized it until now--that's what RESCUE is doing that's different from EVAL. EVAL will give you a definitional error but only from the last operation.

This does imply that maybe RESCUE could be phrased as a refinement to EVAL, to say "return an error early from steps that give ERROR!".

EVAL:TRAP may actually kind of make sense here.

When you think about what TRAP does, it's a tool for functions when you are in the middle of them in an execution step, and you want that step to bubble the error out.

You don't need to use TRAP on the RETURN of a function, the ERROR! just gets passed to the RETURN.

So if you're asking an EVAL to TRAP, you wouldn't mean "on the last step". You must mean on its intermediate steps.

There's probably other options like EVAL:INTERCEPT or EVAL:ABRUPT but at the moment I kind of like EVAL:TRAP.

I definitely like the idea of getting rid of ENRESCUE, so this direction is appealing. More as the situation evolves.

1 Like

So here is some code for CATCH being used to implement QUIT:

catch [
    set (extend mod 'quit) make-quit throw/

    wrap* mod body  ; add top-level declarations to module
    body: bindable body  ; MOD inherited body's binding, we rebind
    eval inside mod body  ; ignore body result, only QUIT returns value
    throw ~
]
then ^arg-to-throw -> [  ; THEN with ^META receives ERROR!, PACK!, VOID!
    ignore ^product: ^arg-to-throw
]

Our default assumption is that you don't want a script to actually return the INTEGER! of 1 when you say quit 1. So MAKE-QUIT is used to create a function that will wrap the THROW with behavior to turn it into a function that will return an ERROR! instead when given an integer (it turns quit 0 into just a TRASH!, as opposed to an error holding an exit code of 0...the same thing you get if a script exits normally.)

But a script can actually return anything when invoked with DO, so there's a refinement to flip the interpretation, e.g. quit:value 1 means "I actually want this script to synthesize the value 1".

All That Aside, Let's Talk Results

Because the above code uses THEN it means it assumes that "light null" is reserved for the case where you don't THROW.

In that presumption, you can't throw null and expect to run an ELSE.

catch [
    throw null
] else [
   ; this would not run, under the assumption
]

This concept arose from the idea that being able to tell whether you did THROW or not was important. But distorting the result in a non-branching structure is probably not ideal.

Then...if CATCH returned its body result, this could be instead:

ignore ^product: catch [
    set (extend mod 'quit) make-quit throw/

    wrap* mod body  ; add top-level declarations to module
    body: bindable body  ; MOD inherited body's binding, we rebind
    eval inside mod body  ; ignore body result, only QUIT returns value
    ~  ; body result
]

There's some echo of the issues with RETURN, of whether or not to put an implicit call to the QUIT function to allow people to hook it and be guaranteed it would run on exit. (At the moment, RETURN is not run implicitly.)

While I'm not entirely sure what I think here, I do wonder if it's over-reaching to box nulls as the built-in THROW behavior.

catch [
   throw null
] else [
   print "This ELSE would run..."
]

If you want to make your throw "heavy" you can do so:

catch [
   throw: throw/heavy/
   throw null
] then [
   print "This THEN would run..."
]

The reason the initial choice was made was so that the [CATCH...THEN] read as "if a throw occurred that you could catch, then..."

But QUIT doesn't want it, because that linguistic play on "CATCH" doesn't apply. I don't know that in the grand scheme of things it's worth building-in the distortion, as opposed to saying it's [CATCH...a non-NULL throw THEN...].

:thinking:

UPDATE: I realize that if this is what you want to do, then there is the ATTEMPT construct... that is designed for the purpose of being able to either BREAK or CONTINUE, where BREAK will signal an ELSE and CONTINUE will signal a THEN!

https://rebol.metaeducation.com/t/repeat-1-attempt-not-as-useless-as-it-looks/2480

1 Like

Distorting what THROW passed as the result of CATCH causes problems for many useful applications of THROW.

So it should not be distorted (if you want distortion, ADAPT the THROW you get)

This doesn't break light void and light null reactivity. It just means the question an ELSE or THEN is asking isn't "was a throw run or not", it's "was something thrown that wasn't light null or void, -or- did it reach the end of the CATCH with no throw".

The best strategy is I think that the CATCH produce light void if there is no THROW, and if you want a value to "fall out" then throw it at the end of the CATCH block explicitly.

This question has weighed on my mind for six years, apparently. :roll_eyes:

It's a tough one, but I don't think there's that much knowledge and technique coming down the pipe anymore.

Let's look at some usages in practice.

A Usage From Encapping

first-section-by-phy-offset: any [
    sections.1
    catch [
        for-each 'sec sections [
            if not zero? sec.physical-offset [
                throw sec
            ]
        ]
    ]
]

Here you see CATCH serving a fairly typical purpose. It keeps you from having to write something more awkward like:

if not first-section-by-phy-offset: sections.1 [
    for-each 'sec sections [
        if not zero? sec.physical-offset [
            first-section-by-phy-offset: sec
            break
        ]
    ]
]

That makes you write out first-section-by-phy-offset: twice. With CATCH you're able to create a spot for that assignment and name assigning it implicitly with the throw.

Here we're taking advantage of the NULL default result for no throws. If we didn't have that, you'd have to say:

first-section-by-phy-offset: any [
    sections.1
    catch [
        for-each 'sec sections [
            if not zero? sec.physical-offset [
                throw sec
            ]
        ]
        null
    ]
]

That's definitely uglier.

I actually think I'd prefer a rule that said "all CATCH requires you to throw, or you get an error telling you that you forgot to"... kind of like RETURN and FUNC right now:

first-section-by-phy-offset: any [
    sections.1
    catch [
        for-each 'sec sections [
            if not zero? sec.physical-offset [
                throw sec
            ]
        ]
        throw null
    ]
]

The subtle "good" part about this is that it prevents people from ever thinking that a THEN or ELSE on the outside answers the question "was there a throw" The non-distorting nature of the THROW becomes self-evident.

It may not seem like a great feature to say that you get a panic when no throws happen. But it's not useless. And ATTEMPT is now plenty powerful for dropping out results...when you don't care about the difference between heavy null and null or heavy void and void. CONTINUE takes an argument, making it almost identical to CATCH. So the "must THROW" feature kind of distinguishes it.

I'm leaning this way, but let's look at some more data.

Console API Code Usage

if (Term_IO) {
    return rebDelegate("catch [",
        "throw as blob! cond (",
            "read-line stdin except (e -> [throw fail e])",
        ")",
    "]");

So this is interesting. This is actually a case that would be a fitting use of TRAP... prehaps with a dialected component to say "use THROW, not RETURN":

if (Term_IO) {
    return rebDelegate("catch [",
        "throw as blob! cond (trap/throw read-line stdin)"
    "]");

But it's actually better than that, because libRebol API-based C natives function define definitional RETURN already. :exploding_head:

if (Term_IO) {
    return rebDelegate("as blob! cond (trap read-line stdin)");

That's wild. So this case can be simplified pretty well.

One observation I'd make is that the throw in catch [throw ...] didn't seem superfluous to me. Had it been missing, I think what's going on becomes a little less clear:

    return rebDelegate("catch [",
        "as blob! cond (",
            "read-line stdin except (e -> [throw fail e])",
        ")",
    "]");

The throw helps parse it. It helps even on a small example, but you can certainly imagine it being more helpful in a long one:

 catch [
     case [
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
        ... pages of code ...
     ]
 ]  ; hm? oh? I guess that CASE was the result, then?

This is actually a pretty good argument for avoiding RETURN fallout in general. If you've got an applicable operator that can teleport, there's a certain degree of solidity given by saying "any teleports will be pre-announced."

Inside The Module Machinery

ignore ^product: catch [  ; IGNORE stops FAILURE! results becoming a panic
    set (extend mod 'quit) make-quit throw/  ; module's QUIT [1]

    wrap* mod body  ; add top-level declarations to module
    body: bindable body  ; MOD inherited body's binding, we rebind
    eval inside mod body  ; ignore body result, only QUIT returns value
    throw ~  ; parallel behavior to if body had done (quit 0)
]

This shows a case where throwing NULL by default would not have been desired. Wanting NULL may not be as common as I imagine it to be.

I'm not sure what arity-0 throw should return. I would think probably void. That makes it align with CONTINUE.

Another Console Example:

This is some code out of an API call:

   "sys.util/recover [",  // pollutes stack trace [3]
        "catch [",  // definitional quit (customized THROW) [4]
            "sys.contexts.user.quit: sys.util/make-quit:console throw/",
            "result': lift eval code",
        "] then (caught -> [",  // QUIT wraps THROW to only throw integers
            "result': caught",  // INTEGER! due to :CONSOLE, out of band
        "])",
    "] then (error -> [",
        "result': error",  // non-lifted ERROR! out of band
    "])"

This shows the particularly insidious problem with fallout. This makes it look like the THEN runs when there is a throw. But there's no isotope magic going on here... and if this wrapped THROW happened to get a NULL it would skip the THEN, just like reaching the end of the catch without a throw would.


What Tips The Scales: Requiring THROW Is Undoable

No matter what the semantics are, we know that being explicit will work.

There's not a conception of CATCH in which catch [... throw value] wouldn't work.

The only question would be "does it distort light nulls and voids to their heavy forms", and I've concluded no.

People can customize it. A naive implementation:

catch: specialize catch/ [body: append copy body ~[, throw ~null~]~]

The reason it's naive is that making copies may not be what you want--there could be relevant identity. So you'd really have to do some binding surgery on a containment.

But anyway... the tools for doing it "right" are closer at hand than they've ever been.