Should TRAP 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. It struck me as a clunky attempt to parrot existing terminology.

TRAP was created to be a seemingly better name than "TRY". It appeared to have more parity with CATCH, and TRAP/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 trapped or not.

>> catch [10 + 20]
== 30

>> error? trap [make error! "this is *not* a trapped error"]
== #[true]

That particular behavior of trap 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.

So if you're trying to write truly "correct" code that uses a TRAP or a CATCH, you pretty much wind up writing a TRAP/WITH or a CATCH/WITH...providing a function taking an error.

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: trap [...], what's wrong with moving it into the code?

 trap [
     value: some-calculation-that-may-fail ...
 ] then func [e] [
     ... 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 TRAP and CATCH, instead using normal THEN/ELSE/etc. constructs with them:

>> catch [10 + 20]
== ~null~  ; anti

>> trap [make error! "not a thrown error"]
== ~null~  ; anti

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.)

TRAP 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 TRAP...just throw your result:

 catch [trap [... 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, TRAP-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 ENTRAP

I made ENTRAP to address the problem of distinguishing errors from other values, by returning the meta 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 unmeta it to get the result. ERROR! is the only plain value returned...so it is an unambiguous signal of a trapped failure.

>> entrap [null]
== ~null~

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

>> error? entrap [1 / 0]
== ~true~  ; anti  (it's just a plain ERROR! value, not quoted or quasi)

>> quoted? entrap [make error! "abc"]
== ~true~  ; anti  (quoted error, since it wasn't RAISE'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: trap [...]

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

trap [var: ...]

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

Compare:

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

With:

trap [	              
    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 an error, or may hold the data". If you wanted to be "clear" you'd have to call it data-or-error, 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 TRAP 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 error? data: trap 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 trapping 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 error? item: trap [
    someone-elses-array: get-array-may-fail x y z
    pick someone-elses-array index
][
     ; may be an ERROR! 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 TRAP/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.

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 TRAP and CATCH could offer a second output which is the value that "falls out" of the evaluation.

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

>> error
== ~null~  ; anti

>> value
== 2

>> [~ value]: trap [1 + 1] then [print "error!"]

>> value
== 2

>> [~ value]: trap [1 / 0] then [print "error!"]
error!

How slick is that?? Very...

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 false]]
       throw pack [2 true]
   ]
== 2

>> x
== 2

>> fallthrough?
== ~true~  ; isotope

This seems like a much better use of the multi-return ability. And to my mind, that tips the scales to where you should do a THROW as the last line if you want "the block's result".

Then if you want to detect whether a result came from the end of the block or somewhere inside it, you can do as I did above and encode that as an extra parameter.

What About Named Throws... How to Know The Name?

Hmmm.

I'll start by saying I've personally felt a bit skeptical of named throws, compared with "definitional throws". :-/ In systems like C++ that support throwing, you can actually say what types of throws you want to receive. Then you receive an object, potentially re-throwing it.

But in any case, if you catch a named throw you might want to know its name. Though I notice that Red does not tell you:

red>> catch/name [throw/name 304 'red] [red blue]
== 304  ; no indication of if blue or red was caught

If we imagine you did want to know which name you caught, then here we have another competing interest for the multi-return ability... the /NAME of the throw.

I think CATCH/NAME would have to shift it so that the caught value becomes the second result. (You don't want the primary slot in multi-return sequences to be itself able to be a multi-return, as that creates error-prone ambiguity. You essentially always have to receive the multi-return as a pack.) Throws wouldn't be allowed to have NULL names because that would conflate with the pure null from nothing caught. So the name would have to be TRASH or BLANK. (Probably a good use for blank.)

2 Likes