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)
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")
s))
;; 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
Disaster
(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.