Discouraging The Interception of Abrupt Failures

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). I will call it TRAP/ABRUPT for clarity, since we now have definitional TRAP.

By its design, TRAP/ABRUPT 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~  ; anti

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

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

But back to TRAP/ABRUPT. 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, an error intercepted by "TRAP/ABRUPT" 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 intercept 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/ABRUPT.

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.RESCUE would be a step in that direction.

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 intercept (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~  ; anti

>> 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/ABRUPT (!)

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/ABRUPT 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.

Modern 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 permissive and said UNMETA would turn NULL into NULL. :-/ Probably not a good idea.

TRAP/ABRUPT => SYS.UTIL.RESCUE

I did the change so that the function for intercepting abrupt failures is poked into SYS.UTIL to make it clearer that you shouldn't be using it casually.

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 [  ; was null
   ... it succeeded
]

That seems all right. But I made an ENRESCUE as well, that lets you get the evaluation product META'd on success, and the plain error on failure.

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

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

>> trap [1 + 2]
== ~null~  ; anti

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

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

Then the same refinement could be used with RESCUE.

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.

3 Likes

I feel like I should mention that there is a trick that could get you the error or the value if there was no error, without using ENRESCUE...

The RESCUE operation could convert an abrupt failure into a definitional one. Then you could use EXCEPT to handle the error case, while getting your value out the top of the expression in the non-abrupt-failure case:

result: sys.util.rescue [
    if condition [fail "This is an abrupt failure"]
    10 + 20
] except e -> [
    ; handle the abrupt failure
]

But this seems like a headache to me. I'm not sure what it buys you compared to doing the assignment inside the body:

sys.util.rescue [
    if condition [fail "This is an abrupt failure"]
    result: 10 + 20
] then e -> [
    ; handle the abrupt failure
]

Convoluting things to get back either an error or a multi-return pack by virtue of raising the error to distinguish the results just sticks you with a hot potato. And you were trying to de-escalate a situation. ENTRAP does that better without the problems of antiforms.

And I feel like rescue [...] then x -> [...] should naturally mean "this is code I want you to run if you rescued something, pass me the error you caught in a defused way."

But I thought I should mention there was a way to do it.