Haskell and Rust Error Handling

Ren-C is shoring up Rebol's historically problematic exception-based error handling by blending together two mechanics: definitional returns and isotopic errors. This gives what I'm calling "definitional failures":

FAIL vs. RETURN RAISE: The New Age of Definitional Failures!

Pivoting to this line of thinking has some non-accidental similarity to Haskell's Either and Rust's Result, which I mentioned when first sketching out the motivation for change:

The Need To Rethink ERROR!

I thought it would be worth it to make a thread for pointing out similarities and differences, and if there are libraries they use that might have relevant inspiration.

An Out-Of-Band State On The Value Is... Like An Isotope?

Either is fully generic beyond just allowing errors (and Rust has its own generic Either as well). So you can really say whatever you are returning can come in two forms: the left form vs. the right form.

But even Rust's Result container will permit you to make an Error class the "valid" result, as well as the "invalid" result.

So here we see generic way of letting a value carry a bit--independent of what the payload is--saying whether that is a "normal" state or a "weird" state. Then, the system has an assortment of operations that are designed for directing program flow in different ways reacting to it.

Ren-C embraces this deeply: effectively saying that every variable and expression product has the potential of being in this "weird" state... and no array can contain a value in the weird state. But you don't have to do anything special to a value that isn't weird to extract it... you just get runtime errors if you use the weird values where they're not expected.

Rust Tackles Low-Hanging Fruit via unwrap() and ?

In Ren-C, if you don't have handling at the callsite when a definitional error happens, it's promoted to being more like a throw--and most code should not intercept it.

Being more formal by nature, both Haskell and Rust force some handling at the callsite when an error result is possible. You have to define a code path to take if there's an error, or a code path to take if there's not.

But Rust has a couple of conveniences. If you want something like the Ren-C behavior, you can just call the .unwrap() method on the result. It will give you the ordinary value if the function didn't return, otherwise raise the error as a "panic".

There's also a cool shortcut with a postfix operator of ? on the call. This makes it so that if you call a function that returns a Result type from inside another function that returns a Result type, it will automatically propagate the result if it's an error out of the calling function.

Getting this behavior is more laborious in Ren-C:

foo: func [...] [
    x: bar (...) except e -> [return raise e]
    ...
]

Implementing the feature as postfix raises its own problems, so let's just imagine we were trying it prefix:

foo: func [...] [
    x: ? bar (...)  ; we want this to act the same as above
    ...
]

For this to work under the current system, each FUNC would have to define its own ? operator... because much like definitional RETURN, it would have to know what it was returning from in case of a raised error.

Haskell Has Generic Compositional Smarts

As mentioned, the strict compile-time typing in Haskell forces you to be explicit about your reaction when a function returns one of these Either values.

But because Either fits in with monadic/applicative/functor stuff, you can have higher-order operations that can compose together failure scenarios...and gather up failures from several functions or cascade the failure through to where you want.

Ren-C can do this kind of thing as well, such as how you can write higher-order functions like ATTEMPT in usermode, or REDUCE a block of values in one pass with META and get the errors, then react to them later.

But in practice, the lack of a static type system makes this more precarious.

Links To Error Handling Libraries

2 Likes