When Should Evaluative Constructs Return ERROR! ?

Definitional failures have been critical in moving to a robust model of error handling.

They're an ergonomic concept something like Rust's Result<...>, because they effectively multiplex errors onto a function's return value. You're expected to triage errors at the moment they happen at a callsite or they promote to divergent panics...which are not generally intended to be reacted to--your program is conceptually terminated. To make triage easier there is TRAP, the error propagator.

Compared with R3-Alpha and Red's error-handling, this is night and day. They are fundamentally broken/useless. (Curiously: Rebol2 actually had a concept of "hot errors", that leaned in the direction of definitional errors, but that direction wasn't pushed through.)


Non-Terminal Eval Steps That Don't Triage Will Panic

You can triage an ERROR! that's a result of an expression. But only if it's the final step:

>> error? (print "Error is last" 1 / 0)
Error is last
== \~okay~\  ; antiform

>> error? (1 / 0 print "Error is first")
** PANIC: attempt to divide by zero

Any evaluation product that comes after an ERROR! will cause a panic.

GHOST! is not exempt:

>> error? (1 / 0 comment "no free pass for ghosts")
** PANIC: attempt to divide by zero

The reasoning is that just because a function returns a ghost, doesn't mean it doesn't have side effects... or doesn't depend in some way on the previous operation.

Currently, COMMA! isn't exempt, either:

>> error? (1 / 0,)
** PANIC: attempt to divide by zero

This is something I might be willing to bend on, if we believe that:

(
    some expression,
    another expression,
    yet another expression,
)

Absolutely has to be equivalent to:

(
    some expression,
    another expression,
    yet another expression
)

But mechanically you'd have to scan ahead for any number of commas... and make sure you were at the end of the input after consuming all the commas. :man_shrugging:

Plain EVAL would act the same as the GROUP! above

Only the last step can give an ERROR!. Previous steps will panic.

And since branching constructs like an IF or a CASE statement uses EVAL to run their branches, they similarly drop the statement out at the last step. So you can synthesize ERROR! out of branches, without causing the branching construct to panic.

"EVAL:TRAP" (name pending) Gives Error At Any Step

I've proposed EVAL:TRAP as a variation which can stop the evaluation at any step that produces an ERROR!:

>> error? eval:trap [print "Error is last" 1 / 0]
Error is last
== \~okay~\  ; antiform

>> error? (1 / 0 print "Error is first")
== \~okay~\

So the second case didn't make it to the PRINT, but it didn't PANIC.

But How About ANY and ALL...? :roll_eyes:

The answer may be different.

You're supposed to reasonably be able to rely on the idea that if ALL gives you a result, it's the result of the final expression. You thus might be thinking that if you get an error, that error is coming by contract from that last expression. I'd say it seems reasonably clear that anything but the last expression should panic.

On the other hand...ANY is not expected to necessarily evaluate all of its clauses... it's supposed to return the first thing that passes its constraint (non-null as the default constraint). So if it hit an ERROR! early, maybe it should return it?

I'm not sure. I'll have to look at use cases.

And What About Loops?

If loops are willing to return definitional errors out of their body, that makes writing loop wrappers a bit trickier.

First let's ask about MAP-EACH, what should it do?

map-each 'x [1 2 3] [either x = 2 [fail "some error"] [x * 10]]

You're getting behaviors that are somewhat equivalent to APPEND. So it's like you wrote:

list: []
append list x * 10
append list fail "some error"

APPEND doesn't propagate a failure like that. And I don't see any reason why it should.

Next, let's consider wrapping loops... e.g. the current formulation of FOR-BOTH:

for-both: func [var blk1 blk2 body] [
    return unlift:lite all [
        lift:lite for-each var blk1 body
        lift:lite for-each var blk2 body
    ]
]

At the moment, LIFT does not lift definitional errors by default, but panics on them.

So if FOR-EACH is willing to return definitional errors, then you won't get an equivalence between:

for-each 'x [1] [fail "some error"]  ; definitional error result

for-both 'x [1] [] [fail "some error"]  ; panic

for-both 'x [] [1] [fail "some error"]  ; panic

It might seem that given what I say about ALL above, if LIFT:LITE were willing to leave ERROR! as ERROR! (as well as NULL as NULL, and GHOST! as GHOST!) then it would correctly panic on errors that weren't the last step... BUT... consider:

all [
    fail "some error"
    comment "hmmm"
]

This runs afoul of my concept of not allowing a next step to run... producing a loophole in composition that is trying to leverage things like invisibility.

This seems to be the crux of a fairly fundamental problem, regarding the need to be psychic in order to know whether an evaluation is going to be a no-op or not.

If you're not looking at the loop data, but tell from the outside of the FOR-EACH after-the-fact if it had evaluations or not, then there's no way to know if a failure needs to be terminal.

This suggests that loops should probably panic if the body fails. They'd have to do so for any step except the last step anyway, and loops don't have an interface for psychically exposing if they're at the last step or not prior to execution. It defeats composition to require otherwise, and the whole model would have to be redesigned to add this "psychic" aspect... which seems less easy than just saying "no, loops panic if the body fails".

Overall Carry-away: There's Subtleties

Decisions about definitional error propagation appear to be non-obvious.

panic-ing is a conservative default, and then you can get people to explicitly CATCH and THROW the errors if they need to work around it.