Condition Handling for Non-Lispers

One of Common Lisp’s more advanced features is its condition handling system, which is a powerful generalisation of other languages’ exception handling systems. I first became aware of condition handling from Peter Seibel’s Practical Common Lisp, chapter 19. But if I’m honest I’ve only very recently understood what it meant; it seems to assume a higher proficiency with Lisp, and I find the S-expressive examples more of a hindrance than a help. Sorry, Lispers.

So instead I’m going to walk through condition handling, with a more thorough explanation of the rationale, and with pseudo-Python examples. (Though, to be clear, these concepts would work just as well in a static language as a dynamic one, as exceptions do.)

The trouble with exceptions

Suppose we’re writing a library for parsing log files. We have a parseEntry function, which takes a single line and returns an Entry, and we want to add a parseLog function that takes a file and returns a whole list of entries, so we can sort them by severity or whatever. This seems perfectly fine, and we toss together a little function to do this:

def parseLog(file):
    list = []
    for line in file:
        list.append(parseEntry(line))
    return list

But parseEntry could raise (throw) a ParseError, which would unwind the call stack back up to the nearest except (catch) block, and then we’re no longer in parseLog. This means if there are any malformed log entries we cannot parse any of the log with parseLog. If the user of our library wanted to parse the log and only print warnings for each malformed entry, they would be forced to write another function that looks identical to our parseLog except it has a tryexcept block nestled between the for loop and parseEntry:

def parseLogLoudly(file):
    list = []
    for line in file:
        try:
            list.append(parseEntry(line))
        except ParseError, e:
            print e
    return list

The possibility of an exception means that we have to manually build the list, because the recovery mechanism (aborting the try block) and the error handling mechanism (the except block) are tightly bound. Our clumsy exception handling system is actually stopping us from building the abstraction that deals with parsing an entire log. And as programming is essentially the art of abstraction, this is something a language should never ever do.

Separating handling and recovery

Instead of requiring that each try comes with its own set of except blocks, let’s separate these two concepts into two distinct syntactic features. We know that parseEntry might raise a ParseError, so we’ll place a try between the for loop and parseEntry, similar to parseLogLoudly but without the except block.

def parseLog(file):
    list = []
    for line in file:
        try:
            list.append(parseEntry(line))
    return list

At this point the ParseError isn’t being handled, so we’ll still unwind out of parseLog if one is raised. This is fine for the library: if the user of the library wants to stop as soon as they reach a malformed entry, they can just catch the error. But suppose they want a parseLogLoudly function; they’ll want to handle the error further up, but then resume from the try block inside parseLog.

def parseLogLoudly(file):
    do:
        return parseLog(file)
    handle ParseError, e:
        print e
        resume

Now when we raise a ParseError it will be handled within ParseLogLoudly but then we will resume running as though we had just aborted the try block within parseLog, and therefore continue the loop. The handle and resume statements essentially act as an except block for the topmost try in the call stack, allowing us to deal with the error further up without fully unwinding from parseLog.

(Common Lisp uses the term ‘restart’, but I think ‘resume’ is more indicative.)

Providing recovery strategies

There are two problems with the design we have so far. First, why is it always the topmost try that resumes? If we had a parseLogDirectory function that called parseLog we would want handling a ParseError to resume the try inside parseLog, not some file-opening try in parseLogDirectory. Second, there may be a number of ways to recover from a given error, and although the decision of which recovery strategy to take is up to the handle block, it’s the code where the error occurred that most likely knows how to deal with the error gracefully — it already has all the state to hand, and again we want to abstract so that we do not need to know exactly how to recover from low-level errors in code much further up.

The solution to both of these problems is to add recovery strategies to the try blocks. Suppose we have two ways we can deal with a ParseError: we can skip the entry, or we can ask the user to fix the entry so we can read it. (Alright, I’m just stealing these examples from Seibel.)

def parseLog(file):
    list = []
    for line in file:
        try:
            list.append(parseEntry(line))
        recover SkipEntry:
            pass
        recover FixEntry, fixed:
            line = fixed
            retry
    return list

Here we define two recovery strategies: SkipEntry, which just continues with the loop; and FixEntry, which comes with a fixed variable which it uses as the new line, and then does the try block again. This allows us to write wrapper functions which both use parseLog but behave very differently.

def parseLogSilently(file):
    do:
        return parseLog(file)
    handle ParseError, e:
        resume SkipEntry
def parseLogInteractively(file):
    do:
        return parseLog(file)
    handle ParseError, e:
        resume FixEntry, ask("fix entry?", e.text)

Condition handling is much more compositional than normal exception handling, and would be great for libraries like Fabric (a task-based Python SSH library), which actually already contains an ad hoc, informally-specified implementation of half of Common Lisp’s conditions. But they aren’t even that complicated when you get down to it — so why don’t more languages have them?