assert(): A modern How To

Reza Naghibi - August 3rd 2026

Something that has always bothered me has been the assert() statement. While I personally consider the simple assertion statement as part of the bedrock of correct, safe, and supportable software, I feel that it also has come up a bit short in some regards. First, a lot of assert() implementations are simply under powered. While they do provide a bit of useful information, these implementations stop short of providing deeper and more useful context. Second, it’s really confusing when you should even use an assertion. Is it a lightweight debugging statement? Can assertions be used in production? What should I be asserting on? Can I customize how assert behaves? This confusion is compounded by the fact that a handful of implementations even allow for all assertions to be disabled with a simple compile time switch. So taken together, I would say much work still needs to be done around proper assert() understanding and usage.

The first and most important step in understanding how to better leverage assert() is to define when and where these statements should be used. I'm going to outline four areas where assertions can be effective:

The added bonus here is when assertions are properly used across your code, static analysis of your code can now become more effective. Assertions greatly narrow the scope for data validation and invariant analysis, allowing for better results. You might be wondering, won’t all these assertions make my code slow and bloated? The simple answer here is no, when used properly, assertions have little to no overhead. We are talking about just a few CPU cycles to read a local value and then skip over a branch, which for most applications cannot even be accurately measured. When evaluated against the benefits assertions are giving you, it’s hard to argue against them.

Correctness and Safety

So let's talk about correctness, because this is probably the most important and under utilized role for assertions. When writing code, it’s easy to overlook all the different potential values a parameter or return value can have. An assertion can fix that gap, simply assert that the value is what you expect. If you support every possible value, which is usually the case, then no assertion is needed. Between the logic and your assertion, all potential values should now be covered and we can now say that coverage for this value is “correct”.

For example, let’s say we have this logic where error values are correctly covered with an assertion:

var error = system_call(...);
assert(!error);

Now, let’s say an error is generated and returned. Your program crashes. Obviously this is not ideal behavior, common values, including errors, should be handled more gracefully. However, what’s important here are two things. First, you have been made aware that this asserted value does exist. You can now choose to take action on it, like making a code change to actually handle this value or maybe spend some time better understanding what happened. Second, and this is the more important point, you started off with 100% value correctness and continue to have 100% value correctness. A failed assertion is behaving exactly as you intended, it correctly caught an edge case. Had that assertion not existed or been compiled away, your application would be in unknown and unsafe territory.

Here is another example illustrating value correctness:

var x = random();
assert(x > 0);

We set the assertion condition that we expect x to be greater than zero and as long as the logic using x supports all possible values of x greater than zero, we can say we are correct here for x. Now, let's say for whatever reason random() gives us a zero or a negative number, the assertion tells us our assumption was wrong and the program immediately terminates. We now hopefully have the opportunity to rectify this error with better handling or at least an investigation of what went wrong. Again, if we wrote this code without the assertion and we encountered a zero or negative number, we now have unexpected behavior and even worse, we may not even know it’s happening.

A few common examples of hard to handle values might be when a memory allocation call fails. While there are some valid ways of handling this, most of the time triggering an assertion and exiting is a valid thing to do. What would you do when a lock fails or a thread fails to spawn? Again, an assertion covering these cases is pretty reasonable given the extreme rarity of these events.

Another important reason to make sure you have 100% value correctness is that sometimes the environment you test on is not the same your application runs on. Subtle behavior differences between versions, APIs, libraries, and platforms can wreak havoc on an application, potentially introducing very hard to debug behavior. So having full value coverage is a way for your application to defensively and proactively combat this kind of silent breakage, immediately. And again, if your application faithfully handles all possible input and output values, then you can safely ignore this advice.

Operational safety is another big area where assertions can greatly help make things better for your application. The classic example is we have raw access to memory, how do we make sure those accesses are what we expect and want (ie: safe)? This comes down to 2 things. First, we must be able to define what the bounds of that memory are, and once we define those bounds, then we simply assert that we are within the bounds when accessing said memory. A lot of times when dealing with well defined types, these bounds and checks can be done automatically. When dealing with less defined types, like a C array or a raw memory pointer, a bound always needs to be defined and assertions applied on that bound when accessing.

Another classic example of operational safety is doing something as simple as:

var c = a + b;

How do we know that we didn’t overflow c when adding a and b together? How would your application behave if c is not a + b? Do you handle the case where c is zero? If these are unknowns, then a simple assertion can fix this (this assumes non negative values):

assert(c >= a);

If this seems overkill for something as simple as an addition statement, then it’s best to think about what bounds are in place in your application which would prevent a and b from getting to the point of potential overflow. Are these values tied to logic, a resource, or other code structure which has a limiting factor? If there are structural limits in place, then a development or documentation assertion might be in order. If no natural bounds exist, then it might make sense to add some safety assertions to your code or switch to an overflow safe library because unknowingly reaching overflow limits on variables is usually a sign that there might be some unsafe things brewing in your application.

Development and Documentation

Next up we have development asserts. These assertions typically cover logic and state errors. When coupled with proper testing, these assertions can be skipped on production code, but not always. An example of a development assertion is guaranteeing we do not have an “off by 1” error when using an index based API. This is common when dealing with strings. For example, let’s say we have a filename and we want to split out the name and extension:

var ext_position = filename.indexOf(“.”);
var name = filename.substring(0, ext_position);
var extension = filename.substring(ext_position + 1);
assert_dev(name + “.” + extension == filename);

In this example, we do a development assertion to make sure we parsed the filename correctly and at the right indexes by re-joining the parts and making sure it matches the original. As long as this is tested and the assertions don’t trigger, we are fairly certain that this logic is correct and it’s safe for this assertion to be skipped in production code. You might say this is a good candidate for a unit test and you are probably right. You could also say that this single assert statement is the unit test and no actual unit test is needed. A good rule is that development assertions usually operate at a level too granular for a full unit test. However, for a development assertion to have any value, it needs to be regularly tested. It’s worth noting that these kinds of assertions can have more execution overhead than simple value assertions, so be aware that if you are running a high concentration of expensive assertions, it can slow things down. This might be ok for testing, not great for production code.

Documentation asserts are similar to development asserts, except they act as reinforcement to document what assumptions any given piece of logic has made. They may seem redundant or even a case of over asserting, but they are really there to help aid with development. These assertions do double duty by both helping a code reader reason about (or even remember) how any piece of logic may operate and also guarding against incorrect usage. Large and evolved code bases which go through refactoring rounds and have logic spread across many source files can really benefit from these kinds of documenting asserts.

Extending the above example, we can add a documenting assert:

var ext_position = filename.indexOf(“.”);
assert_dev(ext_position > 0, “filename must have a name and extension, see validate_filename(): ” + filename);

Now if we hit this assertion, we ask ourselves, what happened to our validation?

When adding new logic to an already complicated codebase, it’s sometimes good practice to work your way through the logic with development assertions. Not only have you added a layer of documentation, but you have added guardrails to assist in future debugging and development work.

If you feel like an assertion is redundant, make it a development only assertion. If it still feels redundant and overly verbose, then you probably don’t need that assertion. Remember, development assertions are like unit tests. While they may be unsightly, I can’t ever recall someone complaining that a codebase has too many tests. When you start effectively using assertions and start building that feedback loop between correctness and your development process, you will start to get a better sense of where assertions provide value and just how valuable they are. Of course, your assertions are only as good as your ability to test them. And having too many assertions triggering in production code is never a good thing.

Looking Forward

So what does an ideal assertion API look like? First, it needs to have both a production and development API. Production assertions can never be disabled and development assertions should be easily switched on and off. Second, it needs to support dynamic messaging. An assertion like this tells us only part of the story:

assert(state == CONNECTED, “invalid state”);

This is a better way to relay the assertion:

assert(state == CONNECTED, “invalid state found: ” + state);

Third, it should support printing a stack dump along with the assertion message. If you hit an assertion deep within code, that stack dump gives you the much needed context as to how you reached that code. Finally, bonus points if you are able to store and then dump important parts of your application state, context, and/or metrics during the post assertion process. This will further assist in helping debug what went wrong that led to that assertion being triggered.

Blog

Home