In JVM Clojure, Exceptions are for operating errors ("something went wrong") and Assertions are for programmer and correctness errors ("this program is wrong").
An assert
might be the right tool if throwing an Exception isn't enough. Use
them when the assertion failing means
- there's a bug in this program (not a caller)
- what happens next is undefined
- recovery is not possible or desired
Use assert
when its condition is so important you want your whole program to
halt if it's not true. (Note for library authors: this is a high bar, because
the program isn't yours.)
An assertion is not the orthodox tool for...
- checking arguments to a public function
- note:
defn
's:pre
and:post
are assertions
- note:
- validating web requests, user data, or similar
Asserts are for "this should never happen" situations, not run-of-the-mill failures.
- catching logically-impossible situations
- checking that the program is written correctly
- ensuring invariants hold
- validating assumptions at dev time without affecting production performance (optional)
- building part of a system in a design-by-contract style
- e.g. in internal code, testing conditions that you believe will be true no matter what a client does
- this is one intended use case for
:pre
/:post
-conditions
Oracle's Java documentation, Programming With Assertions
While the assert construct is not a full-blown design-by-contract facility, it can help support an informal design-by-contract style of programming.
John Regehr, Use of Assertions:
An assertion is a Boolean expression at a specific point in a program which will be true unless there is a bug in the program. This definition immediately tells us that assertions are not to be used for error handling. In contrast with assertions, errors [JVM Exceptions -ed.] are things that go wrong that do not correspond to bugs in the program.
Ned Batchelder, Asserts:
ASSERT(expr)
Asserts that an expression is true. The expression may or may not be evaluated.
- If the expression is true, execution continues normally.
- If the expression is false, what happens is undefined.
Tiger Beetle, Tiger Style:
Assertions detect programmer errors. Unlike operating errors, which are expected and which must be handled, assertion failures are unexpected. The only correct way to handle corrupt code is to crash. Assertions downgrade catastrophic correctness bugs into liveness bugs. Assertions are a force multiplier for discovering bugs by fuzzing.
- "Ceci n'est pas une Error" section in my Idiomatic errors in Clojure article
A catastrophic correctness bug is the kind of error that would cause more permanent damage, so you'd rather the application crash and fails fast. The challenge with assertions is that, what if the issue happens only for a small fraction of user-interaction with the software, but where the software crashes, it impacts all other user-interactions. This is why people don't use assertions very much, because you might not want to crash the whole application, you could crash only those user-interactions that would cause corruption or wrong results, returning an error back to the user, instead of crashing.
Thus the scope of when to use assert has gotten muddy. What kind of bug is so bad you should render the software unusable for all its users and for every user-interactions it supports, even those not impacted by the bug?
Hence asserts are often disabled in production or shipped software, and so people will
(set! *assert* false)
, not to risk an assert crashing the whole application. That said, Clojure*assert*
defaults to true, and people often get burned by that.All this taken into account, and people gradually just stopped using asserts all together, and this goes for
:pre
and:post
as well, since they are assertions under the hood.In reality,
:pre
and:post
would be a great place to validate that a function that expects a string is passed a string and not an int, for example the truss library leverages pre/post in that manner. But once again, because a wrong argument causes a program crash, people avoid it, because you might be calling this function with anint
instead of astring
in only 1% of user-interactions, and once again, why crash it all when 99% of user-interactions are still functional?I think in practice, nobody is able to think of a program condition that is deserving of an assert. What happens is, you fail-fast by throwing a runtime error, and logging it/reporting it. Then you inspect if that error needs an immediate fix or not. This also protects against catastrophic correctness bugs, and today's logging/alarming/reporting means it's as easy to detect as a liveness bug, without uneedingly increasing the blast radius by crashing the whole app when only a portion of it is faulty.