Skip to content

Instantly share code, notes, and snippets.

@daveliepmann
Last active March 2, 2025 12:24
Show Gist options
  • Save daveliepmann/8289f0ee5b00a5f05b50379e07fceb76 to your computer and use it in GitHub Desktop.
Save daveliepmann/8289f0ee5b00a5f05b50379e07fceb76 to your computer and use it in GitHub Desktop.
A guide to orthodox use of assertions in Clojure.

When to use assert?

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.)

Bad use cases for assert

An assertion is not the orthodox tool for...

  • checking arguments to a public function
    • note: defn's :pre and :post are assertions
  • validating web requests, user data, or similar

Asserts are for "this should never happen" situations, not run-of-the-mill failures.

Good use cases for assert

  • 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

References

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.

Further resources

@didibus
Copy link

didibus commented Dec 7, 2024

I tried saying this in less words but couldn't haha. So here goes...

I think we all value a separation between detecting errors that are bugs in your program, the kind that shouldn't have happened, and those errors that are part of normal operation and are anticipated to occur at runtime.

Let's call the former assertion errors, and the latter exception errors.

When you have logic that detects an error state, you have to ask yourself what kind of error it is.

If it's an exception, then it's not a bug, but an anticipated error that you know can happen during normal operation. So you want to handle that appropriately. It could be you retry, or you return an error message to the user so that they correct their erroneous usage, or you attempt an alternate strategy like looking in another folder for the file that was missing, etc.

If it's an assertion error, then it is a bug, not some transient issue or some operational misuses or misconfigurations. So you want to handle that in a way where you're preventing the bug from further corrupting or breaking the state of the application, as well as making sure that the bug doesn't go unnoticed and you're made aware of it so you can debug and fix it.

We have two kinds of errors happening, and we do need to treat them differently.

In order to treat them differently, we need to throw a different type or have a different key on them. So AssertionError vs Exception for example.

Now we can handle Exception being thrown by putting in some retry, alternative fallback strategy, circuit breakers, or returning error messages to the user (be it a person or other program).

The question is what do you do with the AssertionError?

I think this is where I'm arguing for something different.

@daveliepmann If I understood your take, you'd suggest that you don't throw AssertionErrors at all, and you avoid using assertions altogether, especially in library code. Or at least limit their use to private functions.

My take is that, you should instead either disable assertions in production, or have error handling for them in place in your application that does something to let you know of the issue without crashing the app. Or even, let it crash if you're okay with that. And because you're appropriately handling AssertionError in one of those ways, you can now safely use them even more, and they can also be used freely in library code.

P.S.: I think there's another topic related here which is if type errors count as what I'm calling exception or assertion error, and it depends. If the function is called with external input, then it wouldn't be indicative of a bug, but if it's called with internal input it is a bug. My take is a function cannot be called by external input, so it's always a bug. External input comes in from a network IO, or peripheral IO, or file IO, and you should validate that and throw exceptions or what not at the time that input comes in. Once it's at the point where your code is calling other code, if the input is still wrong it's now a bug. So you can assert internal input, but you should validate external input (meaning throw an exception or return some validation error). And for external input validation, you'd likely want it on in production always.

@daveliepmann
Copy link
Author

daveliepmann commented Dec 8, 2024

@daveliepmann If I understood your take, you'd suggest that you don't throw AssertionErrors at all, and you avoid using assertions altogether, especially in library code. Or at least limit their use to private functions.

No, your statements about my take are not correct. I'm in favor of throwing AssertionErrors and of using assertions in library code. I'm just trying to be a stickler for the proper semantics of assertions. I'm mostly not taking a stance on how they should behave or be treated, such as enabling or not in prod or whether to crash or handle them. (Most of your last two comments seem to be about their behavior, not semantics, except maybe your last paragraph.)

I'm not even saying that only private functions should have assertions. I do say that the semantics of assertions do not include checking input arguments to a public function. (And "public function" has a fuzzy meaning in Clojure that you and I seem to differ on.) That public function could still make other kinds of assertions, such as asserting properties of internal data structures after input validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment