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
Does it really matter what type of exception is thrown, NullPointerException, IllegalStateException, IllegalArgumentException, or AssertionError? Probably not as much as you think. Let me explain.
Most of the time, when a library throws an exception like IllegalArgumentException, you’re not writing custom logic to handle it. You’re not wrapping a retry or doing anything specific to recover. Why? Because it’s a bug, a misuse of the library, not something recoverable.
Instead, you’re likely catching it in a generic
(try Exception e)
block somewhere higher up. What happens then? You swallow the exception, log it, and move on. The system degrades gracefully without crashing. But you still have a bug. If you’re lucky, monitoring tools catch it, and you fix it later. If you’re unlucky, the bug lingers unnoticed until it causes significant user complaints.Now imagine the library doesn’t validate its inputs at all. Invalid input might trigger a NullPointerException or some other undefined behavior further down the stack. Debugging becomes even harder because the error doesn’t reflect where the bug actually occurred. Either way, though, you’re just logging and moving on.
AssertionError: What’s Different?
Assertions (AssertionError) fail hard. They don’t let your application degrade gracefully, they crash the thread or process. This makes them unpopular because people don’t want their systems to fail catastrophically. This is why you might complain if a library author threw an assertion error while you won't if they threw a NPE or IllegalArgumentException.
But here’s the thing: you can treat AssertionError the same way you treat exceptions like IllegalArgumentException. You can catch it, log it, and degrade gracefully. This achieves the behavior people want, failing softly, without giving up the benefits of assertions entirely. The old advice to “never catch AssertionError” is outdated. It’s just another signal of a bug, and you can handle it systematically if you want.
By catching AssertionError, you can keep the soft-failure behavior people expect while still benefiting from the clarity and simplicity of assertions for input validation.
External vs. Internal Issues
It’s important to distinguish between internal bugs and external issues (e.g., invalid user input or bad data from the network). Internal bugs are mistakes in your code, things like invalid preconditions or logic errors. These should be surfaced, logged, and fixed.
External issues, on the other hand, aren’t bugs in your application, they’re user errors. In these cases, your application should handle them gracefully by validating inputs, rejecting bad data, and communicating the problem to the user. This is expected behavior and part of the “happy path” for robust systems.
Why Does This Matter?
The real debate isn’t about whether to use AssertionError or IllegalArgumentException. It’s about the philosophy of error handling:
Should we fail hard (crash) when bugs occur?
Or should we fail softly (log and degrade gracefully)?
Failing hard forces immediate attention to bugs but risks destabilizing production systems. Failing softly avoids crashes but can let bugs go unnoticed.
My take is that, in devo, it's fine to fail hard, and people wouldn't mind if libraries had asserts in devo. In prod it's not fine to fail hard, and people do mind if a library throws an AssertionError because it'll fail hard their application. But what I question is, if you want prod assertions for safety, then just catch AssertionError, you don't have to let those fail hard. The idea that an AssertionError fails hard only makes sense in devo, where you want to very obviously realize their is a bug. And I see no difference in production between a library throwing IllegalArgumentException and AssertionError, except that you can't disable the throwing of IllegalArgumentException if you wanted too.