• entries
  • comments
  • views

Epoch Error Handling, Part 2

Sign in to follow this  


One excellent comment on the previous entry on Epoch's error handling philosophy observed that the design really looks an awful lot like exceptions, and doesn't do much for the orthogonal separation of the four points of error handling I enumerated at the top of that post.

Let's revisit those points:

  • Propagating error information as failures occur
  • Adjusting control flow in the presence of error circumstances
  • Maintaining certain contractual semantics (this file will get closed, that memory will get freed, etc.)
  • Responding to and possibly recovering from error conditions

    In the last post, I introduced guard { } entities and the evacuate() function. Together, these address the first two points. evacuate() takes an optional parameter, which is stored in a special out-of-band register in the VM, and is recovered by the guard { } entity and (optionally) passed to a recovery function.

    An obvious question at this point arises: why tie error information with the stack unwinding mechanism of evacuate()? Consider the alternative:

    foo : () -> ()

    entrypoint : () -> ()

    recovery : (42) -> () { debugwritestring("Error in foo") }

    The only real difference is that we need two function calls now: one to assign the out-of-band error information into the magic register, and one to perform stack unrolling.

    What does this really gain us? Two things: first, we can now set error information without stack unwinding; and second, we can unwind the stack without propagating error information. This seems like it really gets the orthogonality of the two points being addressed here.

    Unfortunately, the wins aren't all that great. We already had the ability to unwind the stack without propagating an error: just call evacuate() and let the nearest guard { } catch it. And if we can set error information without acting on it, all we really have done is create a special global variable, with all the evilness and treachery that that implies.

    So in my book, it makes sense to leave these two concerns tightly coupled. Error propagation and stack unwinding make sense together, in my opinion, and they'll probably remain connected in Epoch unless someone has a great argument against doing so.

    The other two points on the list, though, are where things get interesting. Let's consider recovery mechanisms first.

    Since Epoch allows nested function definitions, and also allows use-before-define code style, it is trivial to organize your error handling any way you deem appropriate. Want a global error handler? Easy! Just pass a global function to guard { }. Want a local error handler that uses a function's internal state to affect how it reacts? Also easy! This was alluded to in the previous post, but is worth looking at again in detail:

    entrypoint : () -> ()
    integer(count, 0)


    recovery : () -> ()
    debugwritestring("Succeeded " ; cast(string, count) ; " time(s) before failing!")

    In essence, guard { } allows us to totally separate the code which handles errors from the code which might raise errors, in any organizational pattern we like. Since guard { } is intelligent about calling the recovery function (i.e. it supports pattern-matched parameters), we can do all manner of cool things as the situation demands. There is no obligation, as there is with exceptions in most languages, to handle errors right there in a catch() style block.

    So what about contract obligations?

    This is where things get really neat in my opinion. Consider the two principal mechanisms for upholding contractual obligations in imperative languages with exceptions: RAII (enabled by deterministic destruction), or finally() blocks (necessary in garbage collected environments).

    Epoch supports optional garbage collection. So we can't always count on RAII. We also can't always count on finally() blocks because if an object is marked for deterministic destruction, it might have been obliterated by the stack unwinding process.

    The solution is to make contracts a first-class language feature. Instead of loosely encoding the notion of "always close this file" or "always release this memory" or whatnot, we allow those concepts to be expressed directly. Moreover, for things like memory management, "release this memory" might simply be deleting a reference in garbage collected mode, or actually doing reference counting and deterministic destruction in manual mode. Think of it like having a smart pointer wrapping every memory allocation in your program, by default.

    This is a bit more hazy in terms of syntax potential, which is mostly why I didn't expand on it too much in the previous post. There are a lot of options for how to make this look, and it will take some seriously careful thought to come up with a good approach that'll survive long-term.

    I honestly haven't got a clear idea of what to make the code look like for this, but it relates heavily to the object philosophy of the language itself - something which is still heavily in flux and needs a lot more thought before I can unveil it.

    Here's a quick teaser, though:

    • Data structures do not directly allow methods to be attached to them
    • "Contracts" tie a data structure instance to a set of invariants, which are upheld by code
    • You don't invoke methods on a contract, you send it a message
    • Message passing is uniform for both contracts and parallel processing, both in syntax and implementation
    • This means you can distribute computations based on contracts without changing your code!
    • Yes, I'm stealing heavily from Erlang.
Sign in to follow this  


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now