Skip to content

Instantly share code, notes, and snippets.

@daveliepmann
Last active December 8, 2024 07:52
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 Nov 30, 2024

A catastrophic correctness bug is one that can cause a program to produce wrong results or corrupt data while continuing to run. These are particularly dangerous because the program appears to be working while silently doing the wrong thing.

A liveness bug is one that causes the program to crash, hang, or otherwise stop working. While bad, these are usually more obvious and easier to detect since the program stops working entirely.

A catastrophic correctness bug is the kind of error that would cause more permanent damage, so you'd rather the application crash and fails fast. The challenge with assertions is that, what if the issue happens only for a small fraction of user-interaction with the software, but where the software crashes, it impacts all other user-interactions. This is why people don't use assertions very much, because you might not want to crash the whole application, you could crash only those user-interactions that would cause corruption or wrong results, returning an error back to the user, instead of crashing.

Thus the scope of when to use assert has gotten muddy. What kind of bug is so bad you should render the software unusable for all its users and for every user-interactions it supports, even those not impacted by the bug?

Hence asserts are often disabled in production or shipped software, and so people will (set! *assert* false), not to risk an assert crashing the whole application. That said, Clojure *assert* defaults to true, and people often get burned by that.

All this taken into account, and people gradually just stopped using asserts all together, and this goes for :pre and :post as well, since they are assertions under the hood.

An assertion is not the orthodox tool for... checking arguments to a public function

In reality, :pre and :post would be a great place to validate that a function that expects a string is passed a string and not an int, for example the truss library leverages pre/post in that manner. But once again, because a wrong argument causes a program crash, people avoid it, because you might be calling this function with an int instead of a string in only 1% of user-interactions, and once again, why crash it all when 99% of user-interactions are still functional?

I think in practice, nobody is able to think of a program condition that is deserving of an assert. What happens is, you fail-fast by throwing a runtime error, and logging it/reporting it. Then you inspect if that error needs an immediate fix or not. This also protects against catastrophic correctness bugs, and today's logging/alarming/reporting means it's as easy to detect as a liveness bug, without uneedingly increasing the blast radius by crashing the whole app when only a portion of it is faulty.

@cch1
Copy link

cch1 commented Dec 4, 2024

Why does an assert have to crash the app? With appropriate try/catch it's easy enough to let asserts bubble out to a handler that logs them appropriately and aborts something smaller than the whole program. I use asserts as a nuanced failure -more alarming and fatal than an Exception but not necessarily worthy of shutting down the program.

@didibus
Copy link

didibus commented Dec 4, 2024

@cch1 You catch Throwable? Or specifically catch AssertionError?

I guess you can, but because AssertionError inherits from Error/Throwable, it is easy to forget, since it's common to catch only Exception.

@cch1
Copy link

cch1 commented Dec 4, 2024

@cch1 You catch Throwable? Or specifically catch AssertionError?

I guess you can, but because AssertionError inherits from Error/Throwable, it is easy to forget, since it's common to catch only Exception.

Indeed I catch AssertionError at the outermost periphery of the entry points into our codebase. I consider it a "feature" that AssertionError does not inherit from Exception since "normal" domain code probably has no business trying to recover from AssertionError. At the outermost periphery I can log and terminate the request/event handler without shutting down the program. That's a good compromise IMO.

@daveliepmann
Copy link
Author

@cch1 My first aim with the gist was to describe when to use asserts as distinct from normal exception handling. You, I, and Ned Batchelder's reference above agree that as long as assert's semantics are met, there are several legitimate options for subsequent program behavior.

@didibus
Copy link

didibus commented Dec 4, 2024

@daveliepmann

as long as assert's semantics are met, there are several legitimate options for subsequent program behavior

If that's the case, you might want to remove the quote from Tiger Beetle that suggests: "The only correct way to handle corrupt code is to crash"

Also this statement: "Use assert when its condition is so important you want your whole program to halt if it's not true"

Could be reworded to when you want to "fail fast and prevent undefined behavior from occuring that could result in a worse failure, such as corrupting data or other nefarious effects"

Bad Use [...] checking arguments to a public function

This seems like a legitimate use of assert then no?

@didibus
Copy link

didibus commented Dec 4, 2024

@cch1 I guess you're right, maybe my hung up was bringing Java idioms, where it's considered a bad practice to catch AssertionError as it's meant as a dev/test only tool and defaults to being disabled, to the asserts of Clojure. But maybe Clojure does take a more design-by-contract view, which is why asserts are on by default, and as such you should always consider how you'd handle contract violations in your application, including if you want them to crash your app or not, and that solves the issue of libraries using assert as well.

That said, this taoensso/carmine#301 makes an interesting case against, or at least brings nuance that it should really only be AssertionError that gets caught. Even then though, if people don't look at their logs, it might hide failures as opposed to letting them crash.

@daveliepmann
Copy link
Author

@didibus the gist's goal is to describe orthodox use of assert semantics. Both the quote and the "halt" statement work towards that goal. Those who want to build custom behavior for those semantics are trusted to do so — I don't want to pile caveats on what are intended to be simple instructions for the default/general case.

Bad Use [...] checking arguments to a public function

This seems like a legitimate use of assert then no?

All the advice I've seen about assertions in general and design documents specifically about assertions on the JVM explicitly list "argument checking of functions called from elsewhere" as not an intended use case. User/caller input validation falls under normal error handling in large part because it fails the definitional test of indicating a programmer or correctness error.

@cch1
Copy link

cch1 commented Dec 4, 2024

That said, this taoensso/carmine#301 makes an interesting case against, or at least brings nuance that it should really only be AssertionError that gets caught. Even then though, if people don't look at their logs, it might hide failures as opposed to letting them crash.

@didibus , I certainly agree that catching Throwable other than AssertionError is a dangerous gamble. Even the simple act of logging Throwables can make the underlying condition worse, resulting in a death spiral. But sometimes that is the price you have to pay for observability in extreme conditions. Personally, I don't catch Throwables other than AssertionErrors in app code, and never yet in my library code. YMMV.

As for the admonition to not make assertions about args to public fns, I see nuance here, where making fundamental (e.g. type) assertions about args is useful. Passing the wrong type is a programmer error, not a user input: an invariant has been violated, and some unit of code is incorrect in some unforeseen way. Proper side effects within the unit of code, and a valid return value from the unit of code is no longer possible. The best outcome is to cleanly terminate the failed unit of code at some higher layer of abstraction.

It's a sign of experienced software developer to know which exceptions/faults/errors/throwables/failures to catch/handle and which to leave alone so that a higher abstraction has an unmolested view of the failure. It's the sign of an even more experienced developer to be able to write those higher abstractions and guide less experienced developers in their proper use.

@daveliepmann , thank you for prompting this discussion. I find error handling to be the most challenging part of writing good code. As somebody once said, any sailor can steer the ship when the seas are calm, but the captain takes the wheel in a storm.

@daveliepmann
Copy link
Author

daveliepmann commented Dec 4, 2024

Passing the wrong type is a programmer error

Only if the caller is the same program. Assertions detect bugs in this program, not code we don't control. Validation at the program boundary is a job for IllegalArgumentException or ex-info, not assert.

thank you for prompting this discussion

Thank you for your comments! :)

@didibus
Copy link

didibus commented Dec 4, 2024

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

  1. Not using them to check input types. This seems specifically related to Java or languages with type checking. It is said in those cases, asserting type is redundant and a bad use, because the language compiler will already assert them. Then they show examples of good usage of assert, and it'll assert "refined types", like that a number is positive. So in Java specifically, asserting type like string? would be bad, because there's a type checker already, but asserting the string is of max 20 in length would be good, because you can only do that with runtime asserts. That means in Clojure where there is no type checker, asserting the type like string? would also be a good use, because nothing in Clojure asserts that already as there are no type checking and therefore runtime asserts are a good way to do that.
  2. Fail hard -- The idea of letting the application crash goes against the concept of resilient software. But if you look at the literature that says to do that, it implies assertions being a dev/test time construct, that's disabled in production/shipped software. That's a very important nuance. You should let assertions crash, and you shouldn't have them on in production, because you shouldn't have production crash. The idea of leaving assertions on in production seems quite unorthodox. Now in Clojure, assertions are on by default, and that throws a ranch into this. Does it mean in Clojure you need to always remember to disable them when shipping? Or does it mean Clojure expects you to handle AssertionError in production?
  3. Design by contract -- What's the relationship of assertions and design-by-contract ? Eiffel's design by contract is a more rigorous form of assertions, where you define on a method preconditions, postconditions, and class invariants (the latter which doesn't make sense in Clojure). Clojure supports the same extent of design by contract as Eiffel with pre/post and assert, minus the class invariants (though validators could be seen as serving a similar purpose). In Eiffel, those "assertions", or "contracts" are checked by default, similar to how Clojure checks pre/post/assert by default, unlike in Java, C and others. This leads me to conclude that Clojure's assert is meant to be more like Eiffel's contracts, and therefore the idioms of how to use Clojure's assert/post/pre should be more similar to those of Eiffel and not those of Java or C.

In light of all these, I'm tempted to conclude that in Clojure you'd want too:

  1. Use assert/pre/post to validate program invariants (program errors, not operational erroneous branching flows). This includes asserting types, or value constraints (refined types), amongst other things.
  2. It emphasizes correctness, and maybe wants to steer you towards using assert/pre/post, so it made assertions on by default, similar to Eiffel.
  3. Like Eiffel, it expects that you'll either disable assertions in production, or otherwise implement a fail fast but gracefully strategy which handles AssertionError so the application doesn't crash, especially for critical systems or systems that need to be resilient like backend services.

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

@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