Road To Condition

written by André Gawron, 08. November 2010

This is the second part of an article series about porting the Lisp’s condition system to Ruby. Come back later for the remaining articles.

I argued in The Diversity of Error Handling why existing mechanics for error handling in most languages are insufficient and ambiguous. They force you to mix up error handling and normal return values vanishing the borders of both. This might lead to chaos and confusion. Therefore Lisp provides a simple, clean and more general interface to build different kind of protocols. One of them is used for error handling.

As I said in the previous article, with exceptions you're able to split up recovering code from the actual code which might raise an exception. The condition system takes this approach one step further: signaling a condition, handling it and restarting. I'm mostly referencing to the book Practical Common Lisp written by Peter Seibel. The section Beyond Exception Handling: Condition and Restarts might give you a more deeply understanding of the system and how it's implemented in Lisp. Give it a try, maybe it helps you to get your head around this better1.

Flow of Error

In each application, there are various levels of executions, mostly referred to as high, mid and low level code. The highest level counts on the mid, and the mid counts on the low to succeed. Since application logic is encapsulated2 in functions3, the different level functions are black boxes - the caller has no idea about the inner working.

Now, if a lower level function fails, the caller has two options: failing on its own or try to recover without the help of the lower level function. If it decides to fail, the highest level code will be in trouble as well. There are two catches though:

  1. as long as a function is able to recover itself and return an expected value to its caller, the caller is fine
  2. with every failing method, the application and the callstack respectively moves away from the context which has caused the error

How is the high function supposed to handle the error of the mid and low function without knowing about the circumstances of failing? Even without knowing the implementations of those functions? The answer is simple: make use of the condition system. Lisp's condition system and its way it's implemented in Ruby gives you, the developer, the possibility to put code which recovers from an error in the failing function4 and the code which decide how to recover in higher level functions.

Conditions

A condition is comparable to an exception. An instance of this class contains information about the circumstances of failing. A new condition has to inherit the Condition class, which saves basic information like a message and a backtrace when signaled:

# to define a new condition,
# a new class has to inherit Condition
class NewCondition < Condition ; end

If no constructor of the child class is defined, the parent will use the first optional parameter as a general message5. When signaling a condition, the instance of given condition will be passed to the bound handlers giving them the opportunity to get detailed information of the error.

Signaling a Condition

Signaling a condition is straight forward, just call the function signal, provide the condition name as first parameter followed up by the condition constructor's arguments. signal then searches for established condition handlers and if handlers are bound to given condition, it will execute them with two optional arguments. The first is the condition object and the second is the return value of a handler which was executed before the current one. That said, every condition handler bound to a condition is called6 as long as it doesn't decide to actually handle the condition with a non-local exit. After the handler's execution, flow of control is passed back to signal which will return normally as soon as no more condition handlers are found, returning the return value of the last executed condition handler.

There's another function called error which is, by definition, used when an error shall be raised. error in respect will call signal and the same procedure as outlined above will be gone through, but as soon as signal returns normally7, the error function will raise an ConditionNotHandledError exception8 resulting in an abortion of the program if the exception isn't catched anywhere9. But how to define a so called condition handler? And how to define one which makes use of a non-local exit? Read on.

# the condition is a symbol with a capital letter.
# an instance will be created automatically.
#
# signal always will return normally
signal :Condition, "message"

# always executed
p "foo"

# error will call signal
# but it'll raise a ConditionNotHandledError exception
# if the condition is not handled
error :Condition, "message"

# never executed
p "bar"

Condition Handlers

As already mentioned, after signaling a condition, signal searches for the recently bound condition handler. A condition handler is a block or function of code which will be executed if a bound condition is signaled. To establish a condition handler, there are two functions: handle and bind10. There's a important difference - the former will make use of a non-local exit whereas the latter does not. Since error will raise an exception as soon as signal returns normally, a non-local exit is needed. This comes with a drawback though: the stack will be unwound back to the call of handle11.

# *conditions is a hash. 
# The key is supposed to be the condition-symbol to bind the handler to
# and the corresponding value is a function (defined, lambda or proc)
# which will be executed if the defined condition was signaled
handle *{condition => handler} do
  # here comes the code which might error
end

# usage
handle :Condition => lambda {|condition|
                               p condition.message;
                            } do

  # error a condition
  # the Condition's constructor expects a message string as first argument
  error :Condition, "bar"

  # not executed
  p "foo"
end

# output: "bar"

handle also supports binding multiple condition handlers:

# binding multiple condition handlers
# remember: both handler arguments are optional
handle :Condition => lambda { p "bar" },
       :Another   => lambda { p "baz" } do 
  error :Another, "message"
end

It even let you define handlers for multiple conditions, but it's getting crowded now:

# a handler for multiple conditions
# maybe it's time to use parentheses.
handle :Condition, {:Another => lambda { p "foo" }},
       :YetAnother, {:Last   => lambda { p "bar" }} do 
  error :YetAnother, "message"
end

# output: bar

Check out the README for more information on defining condition handlers.

After reading the examples, you might say that this is just another implementation of exception-based error handling. Yes, you can fall for that. Here's how it would be done using exceptions:

# exceptional version of the handle function
begin
  raise Exception, "message"
  p "foo"
rescue Exception => ex
  p ex.message
end

# rescuing from multiple exceptions
begin
  raise Exception, "message"
rescue Exception
rescue AnotherException
  p "bar"
rescue LastException
  p "baz"
end

But the difference lies in the details. The examples covers only on one level of execution. Here's no real callstack involved and other players like high and mid are missing. On just one level, you can easily deal with errors since you know the implementation. But what will happen if we incorporate a multi-level application into the example?

Feel free to drop by for the next blog post.


  1. hey, he's a professional writer and I'm not :)
  2. well, at least it should be
  3. or methods, but I'll stick to function for this article
  4. with access to the state of that function
  5. which is accessible through #message
  6. LIFO principle
  7. no non-local exit by a condition handler
  8. in Lisp it would drop into the debugger
  9. I am thinking of terminating the whole application using exit when an non-handled error occurs. Then you're not able to make an error using rescue Exception.
  10. cf handler-case and handler-bind respectively in Lisp
  11. or: back to the establishment of the condition handler bound by handle