January 30, 2016

Failjure: Exception-free error handling for Clojure

I’ve written before about handling errors in Clojure without using exceptions, by making use of ad-hoc monads. In that post, I also referenced Andrew Brehaut’s error monad implementation.

Since then, I’ve written a number of projects that use similar handling, and I thought the time had come to wrap it up in a library, which I’m calling Failjure.

How does it work?

Failjure is a utility library for working with failed computations in Clojure. Instead of throwing exceptions, your functions can return failures, which can be handled manually or with a few built-in helpers.

Here’s an example of some code that uses failjure:

(require '[failjure.core :as f])

;; Write functions that return failures

(defn validate-email [email]
    (if (re-matches #".+@.+\..+" email)
      (r/fail "Please enter a valid email address (got %s)" email)))

(defn validate-not-empty [s]
  (if (empty? s)
    (r/fail "Please enter a value")

;; Use attempt-all to handle failures

(defn validate-data [data]
  (f/attempt-all [email (validate-email (:email data))
                  username (validate-not-empty (:username data))
                  id (f/try* (Integer/parseInt (:id data)))]
    {:email email
     :username username}
    (f/if-failed [e]
      (log-error (f/message e))
      (handle-error e))))

Here, attempt-all is a wrapper around clojure.algo.monad’s domonad macro, called with an error monad implementation. It functions a lot like let, except that if any step returns a failure, it short-circuits, either returning the failure object or, if if-failed is provided, calling the body of if-failed with the failure as its argument.

Java Exception objects are also considered failures. In the above example, a call that could throw an exception is wrapped in try*, which catches and returns the exception the same as if fail had been called.

There also exist attempt-> and attempt->> macros, which you can read more about in the README.

Why not just use exceptions?

That is an excellent question. The above could easily be implemented with exceptions, replacing fail with #(throw (Exception. %)) and attempt-all/if-failed with try/catch. I think there are two major differences to consider.

The first is good habits. Clojure lacks a return statement or equivalent on purpose. It is considered bad form to exit a function anywhere except for the end of it, Removing the ability to throw exceptions encourages this sort of software design.

Second is the ergonomics of defining exception types in Clojure. It’s a pain to use gen-class shenanigans to create new Exception types. On the other hand, creating a typed failure is easy, if you really want to.

(defrecord Disaster [message code])

(extend-protocol f/HasFailed
  (message [self] (:message self))
  (failed? [self] true))

(->Disaster "Something terrible has happened!" :red)

Finally, is differentiating between expected faults and unexpected ones. In Java, exceptions can represent either (although RuntimeExceptions are given special treatment by IDEs). In a program using Failjure, exceptions are plainly unexpected errors, at least those that aren’t wrapped in try*, and so should be treated accordingly.

So go on, give it a shot! I think you’ll find it helps you write safer code without compromising on purity or general compactness.