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
@daveliepmann Sorry if I sound insistent. I think I'm also trying to unravel my understanding.
From my reading and interpretation, I think there are some hard to grasp nuances around assert. I'll try to enumerate what I figured and I'm curious what are your thoughts related to these.
In light of all these, I'm tempted to conclude that in Clojure you'd want too:
One last highlight I want to call out. In Eiffel, contracts cannot always prevent corruption of state, because postconditions and invariants clauses are checked after the state of the class has changed. So it only detects programming errors, but the state would get corrupted from it still. Now, once you've detected state corruption, it'll probably mean other program behaviors are gonna be wrong as it's now operating on corrupt state, and hence it might be you need to crash the application, to prevent further corruption to spread.
In Clojure, this is generally not the case, because of immutability. If a function throws an AssertionError, the state of your application has not been corrupted, because most functions would be immutable and return the new state, thus you've detected the new state is wrong before actually modifying the state of the application. This makes handling AssertionError a lot safer, as it prevents corruption of state from happening, it also prevents further program behavior from being wrong or causing more corruption, and therefore is a lot more resilient.
And similarly, validators in atom/ref/agents also prevent corruption, whereas Eiffel's invariants clause only detects corruption. Because the validator failing will rollback the state change.
It's true in Java as well, you'll often assert that some field on the class is not smaller than zero for example after the method modified it, so if it detects it is, the classes instance is already corrupted.
That distinction is worth thinking about, because I think it changes the equation quite a bit for recommended use of how to handle an AssertionError, if it can indicate corrupted state versus indicate a bug that would have corrupted state but has been prevented by the assertions failing fast (before modifying the state).