By developers for developers.

When Things Go Wrong

Clojure’s Exceptional Handling of Exceptions

by Stuart Halloway

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:

The “with-handler” block sets a handler.

The “map” creates a lazy sequence.

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

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

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.