Discouraging use of TRAP

With definitional errors, the landscape changes considerably for how we think about error handling.

In this world, there are not a lot of good reasons to use what we've been calling "TRAP". (historical Redbol's TRY)

By its design, TRAP will intercept any error in code at any depth. I've shown that when ATTEMPT was based on this, it was not good:

>> attempt [print "Attempting to read file" read %nonexistent-file.txt]
Attempting to read file
; null

>> attempt [print "Attempting but made typos" rread %nonexistent-file.txt]
; null

That's fixed now, because ATTEMPT is based on REDUCE-EACH with a ^META variable.

But back to TRAP. The problem isn't just about typos. It's about the illusion that there's something you can do to react to an arbitrary error...when the constructs you were using didn't even understand it well enough to pipe it through to their output.

In almost all cases, a "TRAP"'d error cannot be reacted to sanely...it has to be reported.

It's for this reason that languages like Rust pretty much enforce a panic

And our case is even more compelling. For example: How many places is it ever a good idea to sweep a typo under the rug, and just run some other code?

The few cases it's legitimate are things like the console...where you trap the error and present it, so the user knows it happens and can do something about it. This sort of thing is nearly the only legitimate usage of TRAP.

Might We Make It Look More "Special" To Discourage Use?

I thought at minimum we should move it to a place that shows it's more of a "system utility" than a "language feature".

So calling it SYS.UTIL.TRAP would be a step in that direction.

And as long as we're discouraging use of this, it seems a bit of a waste to take such a short name. It could be SYS.UTIL.TRAP-ANY-ERROR or similar, and then TRAP could be used for something should be used more often.

(It could act like ATTEMPT, but evaluate to the error as its primary result instead of the body of the block.)

Things to think about. Anyway, I've made some progress on definitional errors in the scanner and with TO and MAKE operations, so some of the things people like to trap (like conversions) should work correctly with attempt now.

For instance, in this finite-integer world... an out of range error:

>> attempt [to integer! "10483143873258978444434343"]
; null

>> attempt [to intgeer! "10483143873258978444434343"]
** Script Error: intgeer! word is attached to a context, but unassigned

>> to integer! "10483143873258978444434343" except e -> [print ["Error:" mold e]]
Error: make error! [
    type: 'Script
    id: 'bad-make-arg
    message: ["cannot MAKE/TO" :arg1 "from:" :arg2]
    near: [to integer! "10483143873258978444434343" ** except e -> ***]
    where: [to args]
    file: '
    line: 1
    arg1: #[datatype! integer!]
    arg2: "10483143873258978444434343"
]

Should be a more specific error, now that I look at that. But I guess it just wasn't.

2 Likes

Speaking of discouragement of using TRAP (!)

I'm doing some triage of various "unimportant" things that broke. One of which was SAVE's compression behavior.

R3-Alpha made it so that when you'd SAVE a script you could ask it to be compressed. The compression could be:

  • false - no compression at all, a normal looking script

  • true - the script would have a header, and right after the closing bracket of the header the compressed data bytes would begin.

    • This kind of script would not be loadable in a text editor, since it would be (likely) invalid UTF-8 bytes
  • script - there would be a header followed by a Base64-encoded BINARY! literal of the compressed data

But the header only says "compress" or not. How would LOAD know whether to look for a BINARY! literal or raw bytes? Since all bytes are legal in raw compressed data, it couldn't know by matching 64#{...} (Actually it probably could, since there are magic numbers that start most compressed data, but this was using a kind of black box compression.)

So how did it do it? Using ATTEMPT, and it was bad:

unless rest: any [ ; automatic detection of compression type
    attempt [decompress/part rest end] ; binary compression
    attempt [decompress first transcode/next rest] ; script encoded
] [return 'bad-compress]

If you have script encoding, the first thing it's doing is trying to decompress it as a binary format. That's just decompressing garbage.

But decompressing garbage can do all sorts of insane things, like interpret noise as a memory request size... and then actually request that amount of memory.

ATTEMPT no longer glosses over this kind of thing, so the out-of-memory error came up to the top...revealing this not-good-idea.

It's an epicycle of why TRAP was so bad, and shows the kind of bad practice we shouldn't be sweeping under the rug (!)

2 Likes

Recent experiences has only confirmed what I already know: intercepting arbitrary errors--of the non-definitional sort--is ALMOST NEVER a good idea.

Definitional errors are pretty much the only kind you can react to.

TRAP has a nice interface for use with definitional errors, when you don't want to use the enfix EXCEPT.

Its interface is reversed from ATTEMPT--instead of returning NULL on failure, it returns NULL on success... and on failure returns the error. There's a secondary multi-return for the return product if you want it. (ATTEMPT should probably have a secondary return result for the error, which would give good symmetry.)

Internally to the system in API calls, unpacking multi-returns from a C API call is tricky. So I usually use something I've called ENTRAP. This gives back a ^META return result overlaid with an ERROR!. So you get:

  • a plain ERROR! on failure

  • A QUASI! item like ~foo~ if the result was an isotope

    • This includes tilde (~) if the code evaluated to a VOID
  • A QUOTED! if it was a normal evaluation

    • This includes Lone-Apostrophe-Quote (') if the code evaluated to a NULL

It's easy enough to test for errors.

result: entrap [
    ...your code being trapped goes here...
]

if error? result [
    ...code responding to the result (an ERROR!)...
] else [
    ...process result (it's META'd)
]

You can't use it sensibly with THEN and ELSE (it would always run THEN). Since it's a special construct we could pick a state to return NULL for... e.g. instead of quoted null, which might be convenient... but probably only if we were premissive and said UNMETA would turn NULL into NULL. :-/ Probably not a good idea.

So If All TRAPs Are Definitional, What's The "RESCUE" Operation?

Other languages have gone with calling general exception catching of this sort "RESCUE".

So we could have something called SYS.UTIL.RESCUE. The word sounds like it should have the interface of TRAP:

sys.util.rescue [
    .. dangerous code
] then error -> [
   ... "then" implies "if we rescued, then run this code w/error"
] else [
   ... it succeeded
]

That seems all right. But having a version called ENRESCUE doesn't make too much sense. :-/

Perhaps the meta behavior on these functions should just be controlled by a refinement.

>> trap [1 / 0]
== make error! [...]

>> trap [1 + 2]
; no error

>> trap/meta [1 / 0]
== make error! [...]

>> trap/meta [1 + 2]
== '3 

Then the same refinement could be used with RESULT.

The error won't be ^META'd either way, so calling the refinement /META is a little confusing...but maybe not confusing enough to outweigh the benefit of reminding you that the result is meta'd in there.

Not Sure, But... Overall Point is TRAP Should Be Definitional

TRAP is a nice word with useful functionality, and we shouldn't sacrifice it for an error handling mechanism that is "unsafe at any speed".

I think SYS.UTIL.RESCUE sounds like it should suffice. It's sort of a "yes, it's there, don't use it" kind of thing (due to the interception of typos and all the other reasons).

3 Likes