Failjure is a utility library for working with failed computations in Clojure(Script). 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:
You can also include the specs via the failjure-spec project, if you're into that sort of thing:
(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))))
The cornerstone of this library, HasFailed
is the protocol that describes a failed result.
Failjure 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)))
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
, a java Exception
or a JavaScript Error
.
Added in 2.1
Accepts a value and a function. If the value is a failure, it is passed to the function and the result is returned. Otherwise, value is returned.
(defn handle-error [e] (str "Error: " (f/message e)))
(f/attempt handle-error "Ok") ;=> "Ok"
(f/attempt handle-error (f/fail "failure")) ;=> "Error: failure"
Try it with partial
!
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 ->>
(or, more accurately, some->
and some->>
),
except that a failure value is short-circuited and returned immediately.
Previous versions of failjure used attempt->
and attempt->>
, which
do not short-circuit if the starting value is a failure. ok->
and ok->>
correct this shortcoming
(defn validate-non-blank [data field]
(if (empty? (get data field))
(f/fail "Value required for %s" field)
data))
(let [result (f/ok->
data
(validate-non-blank :username)
(validate-non-blank :password)
(save-data))]
(when (f/failed? result)
(log (f/message result))
(handle-failure result)))
Added in 2.1
Like clojure's built-in as->
, but short-circuits on failures.
(f/as-ok-> "k" $
(str $ "!")
(str "O" $))) ; => Ok!
(f/as-ok-> "k" $
(str $ "!")
(f/try* (Integer/parseInt $))
(str "O" $))) ; => Returns (does not throw) a NumberFormatException
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.
A version of attempt-all
which automatically wraps each right side of its
bindings in a try*
is available as try-all
(thanks @lispyclouds):
(try-all [x (/ 1 0)
y (* 2 3)]
y) ; => java.lang.ArithmeticException (returned, not thrown)
Failjure provides the helpers if-let-ok?
, if-let-failed?
, when-let-ok?
and when-let-failed?
to help
with branching. Each has the same basic structure:
(f/if-let-failed? [x (something-which-may-fail)]
(handle-failure x)
(handle-success x))
- If no else is provided, the
if-
variants will return the value of x - The
when-
variants will always return the value of x
The assert-with
helper is a very basic way of adapting non-failjure-aware
functions/values to a failure context. The source is simply:
(defn assert-with
"If (pred v) is true, return v
otherwise, return (f/fail msg)"
[pred v msg]
(if (pred v) v (fail msg)))
The usage looks like this:
(f/attempt-all
[x (f/assert-with some? (some-fn) "some-fn failed!")
y (f/assert-with integer? (some-integer-returning-fn) "Not an integer.")]
(handle-success x)
(f/when-failed [e] (handle-failure e)))
The pre-packaged helpers assert-some?
, assert-nil?
, assert-not-nil?
, assert-not-empty?
, and assert-number?
are provided, but if you like, adding your own is as easy as (def assert-my-pred? (partial f/assert-with my-pred?))
.
Added clj-kondo support and indent annotations.
(Re-)added AOT compilation to the new leiningen project. This may help resolve errors with some project configurations.
Fix a deployment whoopsie causing attempt
to have reversed argument order from what is documented
here. It was fine in my REPL, I swear!
USE 2.1.1 INSTEAD
Added attempt
and as-ok->
. Changed from boot to leiningen for builds.
Added ClojureScript support. Since the jar now includes .cljc instead of .clj files, which could break older builds, I've decided this should be a major version. It should in general be totally backwards-compatible though.
Notable changes:
- ClojureScript support (thanks @snorremd)
*try
now wraps its inputs in a function and returns(try-fn *wrapped-fn*)
. This was necessary to keep the clj and cljs APIs consistent, but could break some existing use cases (probably).
Added try-all
feature
Resolved issues caused by attempting to destructure failed results.
Fix bug where ok->/>
would sometimes double-eval initial argument.
Refactored attempt-all
, attempt->
, and attempt->>
to remove dependency on monads
Added assert helpers
This version is fully backwards-compatible with 0.1.4, but failjure has been in use long enough to be considered stable. Also I added a .1 because nobody trusts v1.0.0.
- Added
ok?
,ok->
,ok->>
,if-let-ok?
,when-let-ok?
,if-let-failed?
andwhen-let-failed?
- Added changelog.
Copyright 2016 Adam Bard and Andrew Brehaut
Distributed under the Eclipse Public License v1.0 (same as Clojure).