FAIL-ing with Grace


#1

Now that <skip>-able arguments have been firmed up, and are a confirmed Beta/One feature, it’s time to deploy one of my early ideas for using them… FAIL.

If you haven’t been using FAIL, you should! I’ve pointed out in the past you can just say case [… fail] and that’s enough to get a failure and point to that location. But it now lets you just point at any arbitrary value via a quoted word and it will complain about that word and its value:

>> foo: 10

>> fail 'foo
** Error: foo is invalid: 10
** Where: fail console
** Near: [fail 'foo ~~]

It works for paths too:

>> obj: make object! [field: <I've been a bad, bad field...>]

>> fail 'obj/field
** Error: obj/field is invalid: <I've been a bad, bad field...>
** Where: fail console
** Near: [fail 'obj/field ~~]

So this should be your lazy go-to for reporting errors, you get more information and type less than using PRINTs or longer-winded messages.

But the real strength is implicating callsites, vs FAILs

If you don’t provide a place to blame, the only thing the evaluator knows is the whole stack and the place the failure was.

 my-api: func [x y z] [
     if true [
         if y < 100 [
              fail "Who's failing?  one of those IFs?  Or MY-API?"
        ]
     ]
 ]

>> my-api 1 2 3
** Error: Who's failing?  one of those IFs?  Or MY-API?
** Where: fail if if my-api console
** Near: [
    fail "Who's failing?  one of those IFs?  Or MY-API?" ~...

That’s not a terribly informative error message, but the evaluator doesn’t know any more unless you tell it. Previously you’d do this with FAIL/WHERE and pass it a WORD! from the frame you wanted to complain about. But now, it’s cleaner!

my-api: func [x y z] [
     if true [
         if y < 100 [
              fail 'y ["Value must be >= 100, not" y]
        ]
     ]
 ]

>> my-api 1 2 3
** Error: Value must be >= 100, not 2
** Where: my-api console
** Near: [... 1 2 3 ~~]

Now the IFs are gone from the reporting stack, and you’re indicating the callsite but not the FAIL itself. (Ideally the error message would point to the exact block position where the parameter you are complaining about originated from. But that information is not preserved at the moment. In the future it might be kept…maybe in a special slot that would go unused otherwise in 64-bit builds. So the error report would improve!)

Again, you could be lazy and just say fail 'y and get a fair amount of information. In this case a bit of better information than in the custom message provided–in terms of the right name of the bad parameter!

This isn’t arcane C magic, it’s usermode Rebol!

You can read and help improve FAIL any day you feel like it. Please do!


#2

Sweet. Error handling, one of my bugaboos with rebol. This is a nice improvement and should encourage better programming practice.


#3

It needs to keep getting better… one idea I had was that each module would define its own specialization of FAIL which would identify itself, and would chain the error IDs:

 Rebol [
    Name: "My Module"
    Type: 'Module
    Errors: https://example.com/modules/my-module/e/
 ]

 fail /invalid-foo [foo: 10]

The idea would be that this module’s FAIL would be defined to merge that onto the Errors: URL. So you’d get an error with id https://example.com/modules/my-module/e/invalid-foo.

What you’d want to do would be at that URL to serve a page that had english readable text as HTML, but also some kind of template that could be scraped out and used by the console to pretty print the error in a brief notation. If there was no web server or it were down (or taking too long to reply to a request), you’d just see the error as having the ID and any parameters.

I’ve wrestled with the question of how to merge all these desires together. e.g. what if you want to implicate a value and give it an ID, then you’ve got multiple things going on. Or what if the block wants to provide text as a template as well as variables? I make some notes about this in the source:

; Ultimately we might like FAIL to use some clever error-creating dialect
; when passed a block, maybe something like:
;
;     fail [<invalid-key> {The key} key-name: key {is invalid}]
;
; That could provide an error ID, the format message, and the values to
; plug into the slots to make the message...which could be extracted from
; the error if captured (e.g. error/id and `error/key-name`.  Another
; option would be something like:
;
;     fail/with [{The key} :key-name {is invalid}] [key-name: key]

The time to start thinking about how to make all this elegant is now. We have more tools than ever!


#4

Just to clarify, the data/content stored at: https://example.com/modules/my-module/e/invalid-foo is created by and hosted by whom – the author of the script? Or is this URL to Ren-C project standard error messages (and links to glossary, help, docs, forum etc.)? And a script author could modify the target of that URL to point to their own custom/branded messages?

Even if I’m off on the above, I like that you’re thinking about this and thinking of ways of making error messaging a better experience. In a similar vein I’ve been pondering a dialect to be included within code comments to generate developer To-Do lists or diaries, requirements/use-cases, or to integrate/link to architectural diagrams/flows, stuff like that.

At some point I’d also like to be able to use script metadata to auto-generate a browsable/searchable code-library (like rebol.org’s script library, but more lightweight and customizable), so I can better organize, curate, and query my hundreds of scripts for reusable code. This should be even easier to achieve today with the Beta/One enhancements.


#5

If your module is part of the standard Ren-C universe, then your root URL provided here should be to such a path. If it’s not, then you (or your turnkey error-server provider) provides it.

This is kind of where back in the day I thought the rebol.org vs. rebol.net distinction would come in handy. The .net would redirect subdomains for people, so rebmu.rebol.net would just forward off to wherever that was hosted. If it became important to the community but went down at some point, the subdomain could be retaken and then managed by someone else.

The idea would be that whatever that page happened to serve up, the console could sniff out a formatting template from it. There could be more formal methods, a meta item in a http header telling you exactly how to find it, with fallbacks that were a kind of web scraping.

Workflow

It’s just about being able to write in your https module simple things like:

fail /not-found [code: 404 url: location]

Which uses a FAIL specialization/adaptation that tacks the module’s error URL base onto it. So if the user traps the error they get something like:

>> site: https://example.com/catsurmifuble!!  ; Note: inside joke
>> trap [data: read site]
== make error! [
    id: https://rebol.org/https/e/not-found
    code: 404
    url: https://example.com/catsurmifuble!!
 ]

But if the error gets raised in the console, the console tries to do better than this. It hits fetches URL, and one way or another scrapes out a template something like:

 ["URL was not found:" url]

Perhaps with language-specific responses on that. Which could just be managed by conventional web requests as opposed to coming up with a fancier negotiation system in Rebol itself, e.g. by the Accept-Language sent to the server. Not only would the server be configured to give back readable text in that language, but also have the right version of the template: ["URL no encontrada:" url]

Hence the if you got the error reported (vs trapped) it could say:

>> data: read site
** Error: URL was not found: https://example.com/catsurmifuble!!

The console could hold onto the last error. Then it would know the ID if you said something like why:

>> why
Launching browser: https://rebol.org/https/e/not-found

That’s the kind of flow I’m envisioning here. The code itself reports errors by ID and with their parameters, and you delegate to the web itself for the pretty printing and translation. Worst case fallback you just get the error molded.


#6

Yep, gotcha. Yes, I like it. Maybe introduce the feature barebones at first to see how it’s received and collect suggestions/feedback. I think there are really good possibilities which could stem from this approach, particularly for newbies.


#7

Something to be mindful of is that this is the kind of feature that an intern at a software company could spend all summer on as their sole task, and not necessarily get it right.

Plus, the whole concept or model of what an ERROR! is, where that “flavor” of type goes, what its implications are…that was never worked out in R3-Alpha. Red has no better answers, and is still putting numeric IDs that are Rebol specific in the errors:

red> print mold try [1 / 0]
make error! [
    code: 400
    type: 'math
    id: 'zero-divide
    arg1: none
    arg2: none
    arg3: none
    near: none
    where: '/
    stack: 12296944
]

The meanings aren’t questioned, the “type” isn’t questioned (what’s it for?) The 3-arg limit, fixed at the names arg1, arg2, arg3…also not questioned.

Trying to get something like this redesigned for Beta/One is too ambitious. So what has to happen is that a few things get pinned down. I’m starting with “error IDs are URLs” and “there’s some lightweight comparison operator you can use to check just the last part of an ID”.

>> error: trap [data: read site]
== make error! [
    id: https://rebol.org/https/e/not-found
    code: 404  ; user parameter to this error type, NOT generic "error code"
    url: https://example.com/catsurmifuble!!
 ]

 >> error/id ~= /not-found
 == #[true]

Something along these lines. And I’m trying to think about how to push things like the where and near into the meta information, so they don’t wind up affecting error comparisons but are still available.

(This raises the question of using something like id which the user might want for a named field as part of the error. Maybe they’re just out of luck with that.)

But we need to get the most bang for the buck, committing to important things that aren’t planned to change.