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 6, 2024

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.

@daveliepmann
Copy link
Author

@didibus it's now clear that we don't share a fundamental value here, namely that it's helpful to distinguish assertions from exceptions. My thinking starts from the premise that conceptually, assertions are specifically distinct from error handling. That's their semantics, it's why they exist as a tool.

You're of course free to treat assertions and exceptions as if they were the same. Since my goal with this guide was to convince people to treat them distinctly it means you and I will have a tough time finding common ground.

Cheers!

@cch1
Copy link

cch1 commented Dec 7, 2024

I'm pretty sure @didibus is not suggesting they be treated the same but rather as independent tools that can each have a place in a well conceived app.

@daveliepmann
Copy link
Author

You're probably right, Chris. I was too focused on the semantics side of assertions. When didibus wrote with the behavior side in mind that there's no difference between assertions and exceptions I misinterpreted lines like "I see no difference in production between a library throwing IllegalArgumentException and AssertionError", "The real debate isn’t about whether to use AssertionError or IllegalArgumentException", and (rephrasing the first paragraph) "it doesn't really matter if you throw IAE versus AE".

@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