Pragmatic Programmer: should we all use assert?

In The Pragmatic Programmer the author speaks strongly in favor of DBC: Data By Contract, enforced in many languages including Clojure as assert statements. The writers call these complementary to TDD, wiping out different categories of errors. Nonetheless, the only time I’ve ever seen assert used in practice was when one of my employees was… Well, practicing.

Should we all be using assert?

1 Like

I make heavy use of assert (actually a modified version of truss).

I think it is one of the most important factors in removing bugs, especially in a dynamic language like Clojure.

I’ll even go as far as saying that I see it as a better replacement for most types of unit tests, as long as you have a reliable way of exercising your code. Most of my tests are just responsible for calling various functions, without any actual test, because the assertions inside the functions are strong enough.

Some tips:

Any assumptions you make (ie that a variable is not empty, that a key exists) should be encoded as an assertion. The sooner you make that assumption clear, the easier it becomes to diagnose problems.

My modified version of truss is integrated with scope-capture so that any time an assertion error happens I can jump into the context and evaluate the situation.

During development, when I get an exception that is not an assertion exception, I work on figuring out which assumption I have not added as an assertion, and then add it as early as possible in the call tree.

7 Likes

I think it makes sense in the edges where you are willing to pay any overhead that might occur to enforce the contract (not unlike usage of specs or similar validation). It’s probably the same reason spec function instrumentation is not on by default in production (unless you force it or use a library that does on your behalf). You can also re-bind *assert* to nil/false to disable assertion checks in production, but even this may add a counterintuitive overhead to function calls since they have to check the dynamic binding of var, which is not free (it’s faster if the default value is unchanged, e.g. there is no containing binding form). Profiling would be necessary to determine if it matters for a particular use case though, or if correctness insurance is more desirable.

I think the most obvious areas to always or generously use assertions are in macro validation or similar language-level constructs where the assertion is going to run in relatively limited fashion (you can see this in clojure.core). I have leveraged them for runtime consistency checks in data ingest, input validation, and critical runtime consistency checks. Sometimes you find out where you could have benefited from assertions after the fact and just add them in to prevent future grief (along with expanding the test suite).

1 Like

Spec validation and assertion is like assert on steroid, I do use it at the edge of the app to validate data coming in or going out.

1 Like

Regarding the runtime overhead, have a look at truss as it has almost no runtime overhead. Just a macro that gets compiled to nothing on live.

Spec/malli is useful, and I use it a lot, but it is overkill for most common uses, and more useful at the edges of the app. Truss/assert is more generally useful throughout the app.

Sometimes you find out where you could have benefited from assertions after the fact and just add them in to prevent future grief (along with expanding the test suite)

I think this is the very common - you find out which assertions are useful after the fact and add them to prevent regression.

1 Like

Can you provide a code example to demonstrate what you mean? I like the idea of tying spec more in to my asserts

1 Like

Thanks a lot for the great answer - very useful!

Is your “modified version of truss integrated with scope-capture” public available?
Also, do you have public examples of code using these assertions?

1 Like

Assertions are designed around a specific use case in Java/Clojure. See Programming With Assertions.

They’re not for casual checks, which is why they throw a subclass of Error and not Exception. Thus, they are not designed to be caught. They’re for when things have gone so wrong that you want your program to stop immediately. (This means a significant number of usages in Clojure libraries are inappropriate, unfortunate, and troublesome.)

Here’s Alex Miller on the topic. Emphasis mine because “logically impossible” is a key part of their intended use case:

assertions are designed to be turned off in production…they are designed to catch logically impossible situations that can only occur if the program is wrong. they should not be used to detect invalid input (like a bad web request, bad user input, etc).

Consider throwing IllegalArgumentException or an equivalent ex-info for bad user input.

2 Likes

All throughout our dynamic and even static code, as we write it, we are writing code that assumes Things Are A Certain Way. Good example: any function with parameters.

When debugging Z, we often discover a bad value got passed to X which called Y which called Z and only then did the train leave its tracks. These take a while to figure out.

An assert in X would have taken 30s to write. And it is a permanent asset.

QED. But…

People concerned about performance need to put some trace statements in their code to get a feel for how fast are computers today. And maybe also discover where the time goes in slow apps.

The sort of things we typically use assert for is to blow up if a function is passed nil instead of a Component value (since these are often plucked out of a system map and passed down the call chain so this catches typo’d keys etc).

We leave the assertions on for production code because a) the overhead is so minimal and b) if that bad condition ever happens in production, we want the program to blow up instead of trying to plow on with an NPE!

I just checked and we have just over 500 assert calls in just under 100 files (out of 950 files – a total of 139,304 lines of Clojure code).

2 Likes

There’s: assert - clojure.spec.alpha | ClojureDocs - Community-Powered Clojure Documentation and Examples

But my overall point was that assertion is the act of validating properties at runtime.

Spec is a nice library that can help you validate properties about data and relations between input and output for functions.

Normally I do two things, I spec some key functions and use the spec for generative testing, as well as have instrumentation turned on in development, tests and in beta.

In prod, I turn this instrumentation off, but I keep validation on at the edge.

So the former, that I disable in prod, I call assertion, the difference is I’m okay with assertion failures crashing the app.

The latter, that I keep on in prod, I call validation, and I’m not okay with it crashing the app.

An assertion is supposed to catch an internal to the app bug. Assuming valid input to the program, what could go wrong internally? My assertions are focused on catching those mistakes.

A validation is supposed to catch external to the app bugs, such as being given invalid input, or giving something else invalid input. But those aren’t always bugs, sometimes they’re natural behavior, like we expect that external users or programs might sometimes give us invalid input, or might have misconfigured our environment. So validation helps catch integration bugs between systems, but also it’s a happy path in that we anticipate mistakes and have a known planned strategy to handle those cases in production without crashing.

Spec helps us with both setting up our internal assertions to validate that given valid input and a valid operating environment, we will always behave as expected, and it helps us with setting up validation at the edge, which will allow us to handle cases where users or other programs are giving us the wrong input or are running us within a bad environment.

1 Like

This is the problem with dynamic and/or string-ly typed languages such as clojure, python, js, etc. strongly typed languages avoid all of the problems mentioned. they force you to validate the input or it won’t compile.

I agree with the quote from Alex Miller. if you have asserts in your production code, you’re doing something wrong. if you have time to put in the assert, you have time to handle the error properly.

writing good, solid code in dynamic languages ends up being large amounts of type checks. common lisp even has cond variants that switch on the type.

to answer the original question. no, we shouldn’t be using asserts. we should be writing solid code.

having a check at the boundaries that can crash the program is just opening things up for “ping of death” style DoS attacks. handle your errors.

1 Like

I have to disagree here. F.ex in Java you get zero guarantees about null, or about runtime types of objects in a collection.

4 Likes

To me, asserts is a important tool for ensuring your assumptions are valid. It is a critical part of writing solid code (to clarify, I am not talking about AssertionError or clojure/assert, but rather the general concept of asserting input for validity as much as possible)

Regarding the second point, how do static typed languages deal with these issues at the boundaries of your program? I haven’t used a static typed language in a long time, but surely there is the same chance for error at the boundaries when converting to the statically typed class.

3 Likes

If you’re using validate in the definition I just gave then this is simply not true. Program boundaries are always loosely typed, even in static languages, and if you don’t have runtime validation in your static typed languages you’re opening yourself up to all kind of issues and program crashes as well. You need to implement custom runtime validation at the IO boundary in all languages, even statically typed ones.

If you meant it in the definition I gave for assertion, that’s true to a limited extent, I still find extra assertions would be benefitial in statically typed languages as well. Asserting more on properties, like checking it’s positive, not empty, contains a certain number of elements, etc. still has value. Generative testing is still benefitial as well. And finally, I find the schema definition of most statically typed languages isn’t good enough for all use cases. Especially I find it can’t handle options very well, things like if a field value is X then Y is required, but if it’s Z then Y is optional. The kind of scenarios multi-spec solves for.

That’s why I’d love to have something similar to spec or malli even in Java, to make validation of IO boundary easier.

My feelings for the internal assertions of a program is that tests catch most of them anyways. Tests are assertions when you think about it as well, of specific input scenarios, that’s why I like generative testing and the function spec that can assert properties of input to output.

You’re going to have tests anyways, so either that’s good enough and don’t bother with more, including the extra type shenanigans of a statically typed language, or it’s not enough and then static typing similarly isn’t, what you need then is even stronger assertions of properties and generative testing, or proofs that can similarly assert stronger properties.

What static types are good for in my opinion, contrary to popular belief, isn’t correctness, because of what I just explained, but it’s refactoring and auto-complete. (with some exceptions, Rust provides a lot of valuable correctness checks when you don’t have a GC through it’s static type system for example)

5 Likes

Having asserts in production code can save your bacon.

The problem with that “logically impossible” situations is that, of course, they can happen when you least expect them, such as in a production environment under circumstances you could not imagine and could not reproduce on your machine.

3 Likes

See here for some code demonstrating how to use truss with scope-capture. I also included some example assertions below.

I usually require the assertion namespace like [mattsum.assertion :as must], and then just add in assertions throughout the code as needed: for example (must/have argument-a)

90% of the time, this is just checking to ensure a variable is not nil, or checking that a key exists in a map. But because these assertions are compiled away at production time, I also sometimes add more time-intensive checks, for example like checking that a collection is sorted.

I sometimes add the assertions when I first develop, but most of the time they get added as I debug and find errors. Because of the scope-capture integration, I use assertions almost like persistent “breakpoints”.

Some random examples:

(defn- convert-item-specs->entities [{::keys [editors entity-id] :as db-env} item-specs]
  (must/have entity-id)
  (must/have sequential? item-specs)
  (apply m/deep-merge
         (mapv (fn [i] (convert-item-spec->entities db-env i))
              item-specs)))
(defn register-scripts [page ks]
  (let [ss (select-keys named-scripts ks)]
    (must/have #(= (set (keys ss)) %) (set ks) :data {:msg [(set ks)
                                                      (keys ss)]})
    (update page ::scripts merge ss)))
(defn send-sente-event [{{:keys [sente-fns]} ::sente :as env} uid e]
  (let [{:keys [:send-fn]} sente-fns]
    (logging/log :debug env ::new-task-update-event {:event e
                                                    :msg [uid (first e)]})
    ((must/have send-fn) (must/have uid) (must/have vector? e))))

(defn- must-be-sorted [initial-eids]
  (when initial-eids
    (must/have #(= % (sort %)) initial-eids :data {:msg "initial eids must be sorted"})))
2 Likes