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

@cch1
Copy link

cch1 commented Dec 4, 2024

@didibus , your position mirrors mine very closely, but you said it better. Your observations about immutability acting like a brake on the spread of corruption and the feasibility of recovery without shutting everything down is novel (to me) and a great takeaway.

@daveliepmann
Copy link
Author

@didibus I appreciate working through these ideas. Thanks for the insightful Qs.

  1. The most important distinction is whether the function is called by another program or not. Some third-party caller sending you nonsense data is not a logic error in your program.
    With the caveat that I'm not familiar with "refined types": once we're talking about internal calls, then asserting qualities about input parameters is fully on the table. My preference echoes the clojure.spec advice to leverage expressiveness to constrain values instead of using assertions as a poor man's type checker.
  2. Assertions are traditionally designed so they can be turned off in prod in case the checks they make are computationally expensive. Nevertheless that remains your choice. I don't think it's necessary to enable or disable them when shipping.
    There is an additional consideration: the Clojure library ecosystem contains a sadly non-negligible quantity of assert misuse (e.g. for input validation). It would be cool if those libraries didn't do that. As long as they do it's something consumers have to deal with. I do have the opinion that this doesn't justify the (unfortunately common) use of catch Throwable.
    When it comes to crashing or not I'm persuaded by Ned Batchelder's advice to split semantics and behavior. This document focuses on the semantics side. I don't think Clojure expects AssertionErrors to be handled, but I also think Clojure won't snitch on you if you do. I think you might enjoy this discussion (timestamp 38:36) of the lack of assertions in Go, and whether to crash or not when failing an assertion condition. Sean Corfield makes a similar argument that assertions should be left on in prod which I find generally convincing.
  3. Assertions historically predate design-by-contract. Java assertions try to provide Eiffel-like facilities in addition to assert itself. Clojure's assertions AFAICT are implemented along same design as Java's, meaning they're a better facility for mimicking Eiffel than Java could be. See Java documentation Programming With Assertions especially General Questions.

You make an important point that the kind of assertions one makes in a language with immutable data structures is different from those without. (Perhaps a subset?) And too the focus on bare maps rather than OOP. I don't think we can get entirely comfortable but it's a dose of relief. Nevertheless we chop wood and carry water: consider how to handle expected errors and what invariants deserve being asserted.

I want to avoid giving general advice on the behavior side here. On the topic of "in Clojure you'd want to..." I'll repeat Alex Miller's insight:

pre/post is simultaneously under- and over-used. It’s under-used for true program invariants and over-used for input validation.

Assertions are a useful tool because they're not exceptions. The more they're used for quotidian error handling the more we're forced to treat them indistinguishably from exceptions, eroding their raison d'etre. Maintaining a clear distinction between the two is the goal.

@didibus
Copy link

didibus commented Dec 5, 2024

I appreciate working through these ideas

Thank you as well. The exchange of ideas like this is how I learn.

There is an additional consideration: the Clojure library ecosystem contains a sadly non-negligible quantity of assert misuse (e.g. for input validation). It would be cool if those libraries didn't do that. As long as they do it's something consumers have to deal with

I don't understand this. A call to a library made with the wrong type is not input validation. What is passed to the library function is not external input. This is a bug in your program. You're going to have to make a code change to fix it no?

Maybe I'm missing a detail here?

@daveliepmann
Copy link
Author

daveliepmann commented Dec 5, 2024

I don't understand this. A call to a library made with the wrong type is not input validation. What is passed to the library function is not external input. This is a bug in your program.

I certainly see your point that in some sense, the line around "your program" includes all library code. I just don't think it's a helpful perspective in this context. Rather than trying to argue that so many angels can dance on the head of this pin, let's zoom out.

We're firmly in the realm of drawing imaginary sky castles here. We can draw this line wherever we want. So why draw it here rather than there?

In Java the distinction between input validation and asserting invariants is clear. They strictly differentiate public and private methods and have runtime type checking. The latter notably results in Exceptions (e.g. IAE, NPE, index out of bounds). The Java assertions doc lays this all out explicitly.

In Clojure AFAIK there's no official advice about assertions, nor even design notes. Before 1.12 there wasn't even a commitment that assert would throw AssertionError. (In early versions it didn't!) Our language isn't built around strict OOP with an information hiding obsession, so we get to decide for ourselves what the equivalent of "public method" is.

So my thinking goes like this:

  • it's worth taking at least some guidance from the Java assertions doc
    • its stance is "no assertions for public methods argument checking"
    • in Clojure "public methods" roughly translates to something like "callers are outside this namespace"
  • we want to maintain a strong distinction between assertions and exceptions
  • exceptions are typically the tool for input validation
  • I don't see any benefit in libraries using assertions instead of standard error handling for this

@cch1
Copy link

cch1 commented Dec 5, 2024

In your comments above, @daveliepmann , I wonder if there might be some ambiguity over the term "input". If the "input" is directly sourced external to the program the developer controls, then "bad input" is not obviously a program error and proper reporting by the dev's program is valuable. But if the "input" has been sourced internal to the program the developer controls, or it has been sourced externally and then modified by the program the developer controls, then "bad input" is a probably developer error and error recovery is unlikely.

From what I read above, these two interpretations of "input" get at the heart of the discussion above and bear heavily on the suitability of assertions as a means of signalling "bad input."

Somewhere there may be an analogy with the security mechanisms in place in some languages to manage the "taint" of externally-sourced data.

@daveliepmann
Copy link
Author

daveliepmann commented Dec 6, 2024

I wonder if there might be some ambiguity over the term "input". If the "input" is directly sourced external to the program the developer controls, then "bad input" is not obviously a program error and proper reporting by the dev's program is valuable. But if the "input" has been sourced internal to the program the developer controls, or it has been sourced externally and then modified by the program the developer controls, then "bad input" is a probably developer error and error recovery is unlikely.

We might be talking about the same thing. I can't tell.

For me, there's a huge gulf between, for instance, looking for nils in a sequence either as

well, this sequence could contain nil because we don't know anything about it — after all even Joe Schmoe from way off in another subsystem can call this function! — so let's check first and after that we can be sure

versus the same nil check in the context of

we are already sure that there are no nils here because I specifically wrote this subsystem very carefully such that the only entry point is foo which takes care of nils, and within this realm we preserve such-and-such data structure in a very particular way so if there's a nil in it something has gone so horribly wrong I don't know which way is up. let's assert no nils in bar as a sanity check and a kind of canary-in-the-coal-mine against introducing regressions with future development work.

The former is piecemeal hardening of a porous boundary — in other words, validation of input from some sense of "outside". The latter, being entirely inside a strict boundary, is taking advantage of preexisting input validation to make stronger claims about what must be true internally.

From this point of view, there are vanishingly few situations where a library should be making assertions about what consumers pass it.

@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