Failjure is a utility library for working with failed computations in Clojure. It provides an alternative to exception-based error handling for applications where functional purity is more important.
It was inspired by Andrew Brehaut's error monad implementation.
Add the following to your build dependencies:
(require '[failjure.core :as f])
;; Write functions that return failures
(defn validate-email [email]
(if (re-matches #".+@.+\..+" email)
email
(f/fail "Please enter a valid email address (got %s)" email)))
(defn validate-not-empty [s]
(if (empty? s)
(f/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/when-failed [e]
(log-error (f/message e))
(handle-error e))))
fail
is the basis of this library. It accepts an error message
with optional formatting arguments (formatted with Clojure's
format function) and creates a Failure object.
(f/fail "Message here") ; => #Failure{:message "Message here"}
(f/fail "Hello, %s" "Failjure") ; => #Failure{:message "Hello, Failjure"}
These two functions are part of the HasFailed
protocol underpinning
failjure. failed?
will tell you if a value is a failure (that is,
a Failure
or a java Exception
.
attempt-all
wraps an error monad for easy use with failure-returning
functions. You can add any number of bindings and it will short-circuit
on the first error, returning the failure.
(f/attempt-all [x "Ok"] x) ; => "Ok"
(f/attempt-all [x "Ok"
y (fail "Fail")] x) ; => #Failure{:message "Fail"}
You can use when-failed
to provide a function that will handle an error
(f/attempt-all [x "Ok"
y (fail "Fail")]
x
(f/when-failed [e]
(f/message e))) ; => "Fail"
If you're on-the-ball enough that you can represent your problem
as a series of compositions, you can use these threading macros
instead. Each form is applied to the output of the previous
as in ->
and ->>
, except that a failure value is short-circuited
and returned immediately.
(defn validate-non-blank [data field]
(if (empty? (get data field))
(f/fail "Value required for %s" field)
data))
(let [result (f/attempt->
data
(validate-non-blank :username)
(validate-non-blank :password)
(save-data))]
(when (f/failed? attempt)
(log (f/message result))
(handle-failure result)))
This library does not handle exceptions by default. However,
you can wrap any form or forms in the try*
macro, which is shorthand for
(try
(do whatever)
(catch Exception e e))
Since failjure treats returned exceptions as failures, this can be used to adapt exception-throwing functions to failjure-style workflows.
HasFailed
is the protocol that describes a failed result. This library implements
HasFailed for Object (the catch-all not-failed implementation), Exception, and the
built-in Failure record type, but you can add your own very easily:
(defrecord AnnotatedFailure [message data]
f/HasFailed
(failed? [self] true)
(message [self] (:message self)))
Copyright 2016 Adam Bard and Andrew Brehaut
Distributed under the Eclipse Public License v1.0 (same as Clojure).