Pretty image
Stuart wrote the book on Clojure, and here he reveals one of its strengths: the error-kit Condition system, which gives you greater control and flexibility than traditional exception handling.

If you don’t know about conditions, you should. Conditions are basically exception handling, but with greater flexibility. Many Lisps feature a condition system, and Clojure is no exception (pun inflicted by editor). Clojure’s condition system is called error-kit. In this article, you will learn how to use error-kit, and why you will prefer it to plain old exception handling.

Getting the Code

You don’t need to have bought my book Programming Clojure to understand this article, but why wouldn’t you want to? ;) You can follow along throughout this article by entering the code at Clojure’s Read-Eval-Print Loop (REPL). To install a REPL on your local machine, download the sample code from the book. The sample code has its own home page at http://github.com/stuarthalloway/programming-clojure.

The sample code includes a prebuilt version of Clojure, and the clojure-contrib library that contains error-kit. To launch a REPL, execute bin/repl.sh (Unix) or bin\repl.bat (Windows) from the root of the sample code project. You should see the following prompt:

 Clojure
 user=>

For your reference, the completed sample is included in the download at examples/error_kit.clj.

A Simple Problem: Parsing Log File Entries

To see how error-kit handles exceptions, we’ll create a simple application and perpetrate some errors. Let’s write an app that parses log file entries. Our log file entries will look like this:

 2008-10-05 12:14:00 WARN Some warning message here...

In this imperfect world, it is inevitable that some miscreant will pass bad data to the log file parser. To deal with this, we will define an error:

 (use 'clojure.contrib.error-kit)
 (deferror malformed-log-entry [] [msg]
  {:msg msg
  :unhandled (throw-msg IllegalArgumentException)})

The error takes a single argument, a msg describing the problem. The :unhandled value defers to a normal Clojure (Java) exception in the event that a caller chooses not to handle the error. (The empty vector [] could contain a parent error, but we won’t need that in this example.)

Now, let’s write a parse-log-entry function:

 (defn parse-log-entry [entry]
  (or
  (next (re-matches #"(\d+-\d+-\d+) (\d+:\d+:\d+) (\w+) (.*)" entry))
  (raise malformed-log-entry entry)))

The first argument to or uses a regular expression to crack a log entry. If the log entry is not in the correct format, the second argument to or will raise an error. Try it with a valid log entry:

 (parse-log-entry
  "2008-10-05 12:14:00 WARN Some warning message here...")
 -> ("2008-10-05" "12:14:00" "WARN" "Some warning message here...")

Of course, we could do more than just return a simple sequence, but since we are focused on the error case we’ll keep the results simple.

What happens with a bad log line?

 (parse-log-entry "some random string")
 -> java.lang.IllegalArgumentException: some random string

An unhandled error is converted into a Java exception, and propagates as normal.

The Problem with Exceptions

So why wouldn’t we simply throw and catch exceptions? The problem is one of context. At the point of an exception, you know the most intimate details about what went wrong. But you do not know the broader context. How does the calling subsystem or application want to deal with this particular kind of error? Since you do not know the context, you throw the exception back out to someone who does.

At some higher level, you have enough context to know what to do with the error, but by the time you get there, you have lost the context to continue. The stack has unwound, partial work has been lost, and you are left to pick up the pieces. Or, more likely, to give up on the application-level task that you started.

The Solution: Conditions

Conditions provide a way to have your cake and eat it too. At some high-level function, you pick a strategy for dealing with the error, and register that strategy as a handler. When the lower-level code hits the error, it can then pick a handler without unwinding the call stack. This gives you more options. In particular, you can choose to cope with the problem and continue.

Let’s say that you are processing some log files that include some garbage lines, and that you are content to skip past these lines. You can use with-handler to execute the code with a handler that will replace bad lines with, for example, a simple nil.

 (defn parse-or-nil [logseq]
  (with-handler
  (vec (map parse-log-entry logseq))
  (handle malformed-log-entry [msg]
  (continue-with nil))))

The call to continue-with will replace any malformed log entries with nil. Despite the structural similarity, this is not at all like a catch block. The continue-with is specified by an outer calling function (parse-or-nil) and will execute inside an inner, called function (parse-log-entry).

To test parse-or-nil, create a few top level vars, one with a good sequence of log entries, and one with some corrupt entries:

 (def good-log
  ["2008-10-05 12:14:00 WARN Some warning message here..."
  "2008-10-05 12:14:00 INFO End of the current log..."])
 
 (def bad-log
  ["2008-10-05 12:14:00 WARN Some warning message here..."
  "this is not a log message"
  "2008-10-05 12:14:00 INFO End of the current log..."])

The good-log will parse without any problems, of course:

 (parse-or-nil good-log)
 -> [("2008-10-05" "12:14:00" "WARN" "Some warning message here...")
  ("2008-10-05" "12:14:00" "INFO" "End of the current log...")]

When parsing hits an error in bad-log, it substitutes a nil and moves right along:

 (parse-or-nil bad-log)
 -> [("2008-10-05" "12:14:00" "WARN" "Some warning message here...")
  nil
  ("2008-10-05" "12:14:00" "INFO" "End of the current log...")]

OK, but what if you wanted to do more than just return nil? Maybe the original API signals an error, but doesn’t do any logging. No problem, just impose your own logging from without:

 (defn parse-or-warn [logseq]
  (with-handler
  (vec (map parse-log-entry logseq))
  (handle malformed-log-entry [msg]
  (continue-with (println "****warning****: invalid log: " msg)))))

Now, parsing the bad-log will log the problem.

 (parse-or-warn bad-log)
 ****warning****: invalid log: this is not a log message
 -> [("2008-10-05" "12:14:00" "WARN" "Some warning message here...")
  nil
  ("2008-10-05" "12:14:00" "INFO" "End of the current log...")]

Of course a production-quality solution would use a real logging API, but you get the idea. Slick, huh?

Make Life Simple For Your Callers

It gets even better.

If you know in advance some of the strategies your callers might want to pursue in dealing with an error, you can name those strategies at the point of a possible error, and then let callers select a strategy by name. The bind-continue form takes the name of a strategy, an argument list, and a form to implement the strategy.

So, continuing with our log example, you might choose to provide explicit skip and log strategies for dealing with a parse error:

 (defn parse-or-continue [logseq]
  (let [parse-log-entry
  (fn [entry]
  (with-handler (parse-log-entry entry)
  (bind-continue skip [msg]
  nil)
  (bind-continue log [msg]
  (println "****invalid log: " msg))))]
  (vec (map parse-log-entry logseq))))

parse-or-continue has no continue-with block, so a bad log entry will default to a Java exception:

 (parse-or-continue bad-log)
 -> java.lang.RuntimeException: java.lang.IllegalArgumentException:
  this is not a log message

Callers of parse-or-continue can select a handler strategy with the continue form. Here, the call selects the skip strategy:

 (with-handler (parse-or-continue bad-log)
  (handle malformed-log-entry [msg] (continue skip msg)))
 -> [("2008-10-05" "12:14:00" "WARN" "Some warning message here...")
  nil
  ("2008-10-05" "12:14:00" "INFO" "End of the current log...")]

And here it selects the log strategy:

 (with-handler (parse-or-continue bad-log)
  (handle malformed-log-entry [msg] (continue log msg)))
 ****warning****: invalid log: this is not a log message
 -> [("2008-10-05" "12:14:00" "WARN" "Some warning message here...")
  nil
  ("2008-10-05" "12:14:00" "INFO" "End of the current log...")]

Notice the continue forms pass an argument to the bound continues. In these examples we just passed the error message, but the parameter list could be used to implement arbitrary coordination between continue calls and bound continue forms. This is powerful.

Laziness and Errors

Most Clojure data structures are lazy, which means that they are evaluated only as needed. To make these lazy structures play nicely with conditions (or even plain old exceptions, for that matter), you have to install your handlers around the code that actually realizes the collection, not around the code that creates the collection.

This can be confusing at the REPL. Can you spot the problem below?

 (with-handler
  (map parse-log-entry bad-log)
  (handle malformed-log-entry [msg]
  (continue-with nil)))
 -> java.lang.IllegalArgumentException: this is not a log message

The code above is trying to add a handler, but it isn’t working. Stepping through the sequence of events will show why:

  1. The “with-handler” block sets a handler.

  2. The “map” creates a lazy sequence.

  3. The “handler” block exits, returning the lazy sequence to the REPL.

  4. The REPL realizes the sequence to print it, but by now the handler is gone. Oops.

In the earlier examples we avoided this problem by explicitly realizing the sequence with calls to vec. Here’s the takeaway: In your own applications, make sure to install handlers around realization, not instantiation.

Wrapping Up

Traditional exception handling gives you two points of control: the point of failure, and the handler. With a condition system, you have an all-important third point of control. Handlers can make continues available at the point of failure. Low-level functions can then raise an error, and higher-level functions can deal with the error at the point it occurred, with full context.

If you have ever found a large project staggering under the weight of exception handling code, you might want to consider giving conditions a shot.

Notes

The choice of log file parsing for the example was inspired by a similar example in Peter Seibel’s excellent book Practical Common Lisp.

Copyright 2009 Stuart Halloway. Used with permission.

Stuart Halloway is a co-founder and CEO of Relevance, Inc. Relevance provides development, consulting, and training services based around agile methods and leading-edge technologies such as Ruby and Clojure. In addition to Programming Clojure, Stuart has authored several other books, including Component Development for the Java Platform and Rails for Java Developers.