Learning assertions and how to use them.

An Assert Documents What You Believe, Not What You Hope

I remember sitting in a dim server room during my HFT days, staring at a core dump that looked like a crime scene. We had spent weeks debugging a race condition, only to realize the logic was sound but our assumptions about state were fundamentally broken. Most tutorials treat assertions and how to use them as a trivial “check this variable” checkbox, but that’s a dangerous simplification. If you treat them as mere documentation rather than active enforcement of your mental model, you aren’t actually writing robust code; you’re just writing a polite request for the program not to crash.

I’m not here to give you a lecture on the syntax of `assert()`. Instead, I want to talk about the boundary between what your code should do and what it actually does when the compiler starts optimizing your life away. I’ll show you how to deploy assertions to catch the logic errors that haunt production, and more importantly, where to draw the line so you don’t bloat your hot paths. This is about understanding the contract you make with your code, ensuring that when things fail, they fail immediately and loudly.

Table of Contents

Mastering Preconditions and Postconditions to Guard Your Logic

Mastering Preconditions and Postconditions to Guard Your Logic

Most developers treat assertions like a nuisance, but if you want to write robust code, you need to view them as the enforcement mechanism for your function’s contract. This is where preconditions and postconditions come into play. A precondition is a requirement that must be true before a function even begins its work—think of it as a gatekeeper. If your function expects a non-null pointer or a positive integer, an assertion at the entry point ensures you aren’t processing garbage data. If that check fails, you get a runtime assertion failure immediately, rather than letting the corruption drift downstream where it becomes a nightmare to debug.

Postconditions work in reverse, verifying that the function actually did what it promised before it returns control to the caller. It’s the final sanity check. By integrating these defensive programming techniques directly into your logic, you aren’t just writing code; you are documenting the exact operational boundaries of your system. You stop guessing whether a value is valid and start forcing the program to prove it.

The High Cost of a Runtime Assertion Failure

The High Cost of a Runtime Assertion Failure.

There is a common misconception that a runtime assertion failure is a minor hiccup—a polite way for the program to say, “I’m confused.” In reality, an assertion failure is a controlled crash. When an assertion trips, you are essentially admitting that your internal model of the system has diverged from reality. If you are running a high-frequency trading engine or a real-time control loop, that sudden termination isn’t just an inconvenience; it is a systemic failure that can have immediate, expensive consequences.

The cost isn’t just measured in downtime, but in the cognitive load required to debug it. A runtime assertion failure tells you where the contract was broken, but it rarely tells you why the state became invalid in the first place. This is where the distinction between compile-time assertions vs runtime becomes vital. If you can catch a violation using `static_assert`, you do it during the build. If you wait until the code is executing in a production environment, you aren’t just debugging a program; you are performing digital forensics on a crime scene.

Five Rules for Not Wasting Your Time with Assertions

  • Stop using assertions to check for user input errors. If the error can happen because a human typed something wrong, it’s a runtime error, not a logic error. Use exceptions or error codes for that. If you assert on user input, you’re just building a program that crashes instead of handling mistakes.
  • Keep your assertion expressions pure. An assertion is a diagnostic tool, not a place to mutate state. If I see `assert(ptr = malloc(size))` in a codebase, I’m walking away. If the assertion is stripped in a release build—which it usually is—that memory allocation never happens, and you’ve just introduced a silent, non-deterministic bug.
  • Assert your assumptions, not your results. Don’t just assert that a function returned the correct value; assert that the invariants of your object model still hold true after the function executes. You want to catch the moment the internal state becomes nonsense, not just the symptom.
  • Use `static_assert` whenever you can. If you can prove a condition is false at compile time, do it. There is no reason to let a broken template instantiation or an invalid buffer size reach a runtime assertion. If the compiler can catch it, let the compiler catch it.
  • Don’t let your assertions become a performance crutch. I’ve seen developers write massive, complex logic checks inside an `assert()` because “it’s free in release.” It’s not free if it makes your debug builds so slow that you stop running them. Keep the checks lean and focused on the logic that actually matters.

The Bottom Line

Use assertions to document your assumptions, not to fix your bugs; if you’re relying on an assertion to “handle” an error, you’ve already failed the design phase.

Distinguish between developer errors (assertions) and runtime environmental failures (exceptions) to ensure your production binary isn’t a minefield of unnecessary crashes.

Assertions are your primary defense against the “silent failure” mode of C++, catching logic violations at the source before they propagate into untraceable memory corruption.

The Final Check

At the end of the day, assertions aren’t about making your code perfect; they are about making your assumptions explicit. We’ve covered how to use them to enforce preconditions and postconditions, and why you must be careful not to let them bleed into your production performance profile. If you treat them as a secondary thought, you’re just leaving a trail of breadcrumbs for a debugger to find three months too late. Use them to define the boundaries of your logic, and use them to fail fast before a corrupted state turns into a silent, catastrophic data error.

Writing robust C++ is a constant battle against the complexity of the language and the fallibility of the programmer. You will make mistakes. I make mistakes. The difference between a professional and an amateur is often just how much they trust their own code. Stop treating your logic as an infallible truth and start treating it as a set of hypotheses that need constant verification. Build your safety nets early, test them thoroughly, and let your assertions do the heavy lifting of proving you right—or, more importantly, proving you wrong while you still have the chance to fix it.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Versioning a C++ library for API compatibility.

Api Compatibility and Abi Compatibility Are Different Promises

Steady clock chrono for measuring time.

Use Steady Clock to Measure, System Clock to Report