What I write below might be universal knowledge, but I was personally bitten by this more than once, so I feel the need to emphasize it once again.
Clojure's assert form can be used for quick&dirty verification and consistency
checking in your code. It is convenient to have them during development because
they may discover incorrect usage of functions before the mistake drops deeper
into the callstack and fails with something like NullPointerException
or
java.lang.Long cannot be cast to clojure.lang.IFn
. And for this task assert
is perfectly fine — it is there out of the box, and you can disable the
assertions in production by setting clojure.core/*assert*
var to false.
The problems begin when you stick to assertions for prod-time data validation (once again, you may already be smart enough not do it. I wasn't).
(assert nil "Oops!")
; java.lang.AssertionError: Assert failed: Oops!
(supers AssertionError)
#{java.lang.Error java.lang.Object
java.lang.Throwable java.io.Serializable}
There's your (not-so-)subtle landmine. AssertionError
, as its name implies, is
a subclass of Error
, not Exception
. So, unless you catch Throwable
instead
of Exception
in your high-level crash-recovering code (which is a questionable
practice in itself), the assertion errors will happily fall through the cracks
and cause some of the following:
- crash the execution thread
- fail to log properly
- fail to respond to the client
But if assertions are bad in production, what are the options?
clojure.spec and Schema
With the release of clojure.spec in Clojure 1.9 it will be even less likely
for assert
to be used for validation. Also, Schema was around long enough
for people to know better. These two tools are much more suited for the task.
I can't recommend enough to learn either of them (more so clojure.spec since
it is going to become the de-facto standard soon).
What if you need to check some assumption or invariant midway through the
function? Schema is a bit inconvenient for that (after all, its primary
purpose is automatic validation on function boundaries). Spec has better API
for this case, yet sometimes you want something as straightforward as
assert
.
Truss
Truss is a lightweight assertion library. It has fewer features than Spec and
Schema but is zero-ceremony and easy to understand. You should consider
Truss if you need minimal improvement over assert
and don't want to invest
into comprehensive validation solutions.
Roll your own
If, for whatever reason, you don't feel like pulling a library for such a
seemingly trivial task, you can always write your own drop-in replacement for
assert
. For example:
(defmacro hope
"Check if the given form is truthy, otherwise throw an exception with
the given message and some additional context. Alternative to
`assert` with Exceptions instead of Errors."
[form otherwise-msg & values]
`(or ~form
(throw (ex-info ~otherwise-msg
(hash-map :form '~form
~@(mapcat (fn [v] [`'~v v]) values))))))
(let [coll [1 2 3 4]
pred odd?]
(hope (every? pred coll)
"Not every item matches!"
pred coll))
;; 1. Unhandled clojure.lang.ExceptionInfo
;; Not every item matches!
;; {pred #function[clojure.core/odd?], coll [1 2 3 4], :form (every? pred coll)}
We use this macro in addition to Schema. It is literally just assert
which
throws Exceptions, and with slightly more horsepower. And it is small enough
to shove it into util.clj
or what have you.
In summary, there are plenty of better alternatives to assert
for it to
fade into obsolescence. Choose the one you like most and don't repeat the
mistakes of others.