JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Assertions, Invariants, and Contracts

An assertion is a line of code that says 'this must be true right here, or my whole understanding of the program is wrong.' This guide shows how assert() turns silent broken assumptions into a loud, located crash, why an assertion checks for bugs rather than handles errors, and how invariants and contracts make those assumptions explicit.

Two different kinds of 'something went wrong'

The first two guides in this rung were about errors the world hands you. You learned to read a return code, consult errno after every call, and clean up with goto-cleanup or the RAII idea. All of those handle a situation that is unfortunate but legitimate: the disk is full, the file is missing, malloc() came back NULL. The program asked for something reasonable and the answer was no. This guide is about a fundamentally different beast — the moment when the program's own logic is wrong, when an assumption you were certain of turns out to be false.

Here is the sharp line, and it is the single most important idea in this guide. A recoverable error is something that can legitimately happen to a correct program: bad input, a missing file, an out-of-memory condition. You expect it, you handle it, you keep going. A bug is a broken assumption inside your own code: a function got a NULL pointer it documented could never be NULL, a counter went negative, an index ran off the end of a buffer. A correct program never does these things, so when one happens, your understanding of the program has already failed. This is the heart of recoverable errors versus bugs, and an assertion is the tool built precisely for the second kind.

What assert() actually does

An assertion is a single executable claim about what must be true at a given point. In C it comes from the standard library as the macro assert(), declared in the header assert.h. You write assert(expr), where expr is any condition you believe is always true there. At runtime the macro evaluates expr. If it is non-zero (true), nothing happens at all — execution flows straight on as if the line were not there. If it is zero (false), the assertion has fired: the program prints a message naming the failed expression, the source file, and the line number, then calls abort() to terminate the process immediately.

Picture a function `int divide(int a, int b)` that includes `assert.h` and opens its body with `assert(b != 0);` before doing `return a / b;`. We are claiming a divisor of zero is a bug — it should never reach here. As long as b is non-zero, that line is invisible and costs essentially nothing. But the day b arrives as zero, the program halts at once and prints something like `a.out: prog.c:4: divide: Assertion 'b != 0' failed.`, then aborts. A would-be undefined division by zero has been converted into a clean, named, located stop.

Notice how much that message gives you for one line of code. It names the exact expression that failed (`b != 0`), so you know not just that something broke but precisely which belief was wrong. It names the file and line, so you go straight to the spot. And because abort() typically leaves a core dump, you can load the corpse into a debugger and inspect every variable at the moment the impossible happened. This is the fail-fast philosophy in its purest form: stop at the first sign that your model of the world is broken, while the evidence is still warm, rather than limping onward and crashing later in a stack frame that has nothing to do with the real cause.

Invariants: the truths a piece of code lives by

Assertions become powerful when you use them to pin down invariants — conditions that should hold true at a particular place every single time, no matter what path the program took to get there. The cleanest example is a data-structure invariant. Suppose you write a dynamic array with three fields: a pointer to the buffer, a length (how many items are in use), and a capacity (how many fit). There is a quiet truth that must always hold: length is never greater than capacity. If it ever is, something has corrupted the structure. You can make that truth a checkable, named guard at the top of every function that touches the array.

typedef struct {
    int *data;
    size_t length;     /* items in use            */
    size_t capacity;   /* items the buffer can hold */
} Vec;

void vec_push(Vec *v, int x) {
    assert(v != NULL);                 /* precondition  */
    assert(v->length <= v->capacity);  /* invariant on entry */
    if (v->length == v->capacity)
        vec_grow(v);                   /* make room first */
    v->data[v->length++] = x;
    assert(v->length <= v->capacity);  /* invariant on exit  */
}
The same invariant (length <= capacity) is asserted on entry and on exit, bracketing the function with the truth it must preserve.

Two flavours of invariant appear in that tiny function, and naming them sharpens your thinking. A precondition is what must be true for a function to be allowed to run — here, v is not NULL and the structure arrives intact. A postcondition is what the function promises to leave true when it returns — here, the invariant still holds after the push. You will also meet a loop invariant: a condition you assert holds at the top of every iteration, which is the classic way to convince yourself a loop is correct. And in the concurrency rungs you have already met the lock invariant — the rule that you only touch certain shared state while holding the matching mutex. All of these are the same instinct: write the truth down, then let the machine check it for you.

Contracts: a promise between caller and callee

Zoom out from a single function and a pattern emerges. Every function is a small agreement between two parties: the caller, who promises to supply valid inputs, and the callee, who promises to deliver a valid result in return. That agreement is a contract, and preconditions, postconditions, and invariants are simply its written terms. The deep idea, often called design by contract, is that if both sides keep their half, the whole thing works — and an assertion is how each side checks that the other side kept its promise.

This reframes assertions from scattered safety checks into something architectural. When divide() asserts `b != 0`, it is publishing a precondition: *my contract says you, the caller, must never pass me a zero divisor; if you do, that is your bug, not mine, and I will say so loudly.* This is why an assertion belongs at a trust boundary inside your own program, between modules you wrote. It draws a clear line of blame: a fired precondition assertion points the finger upstream at the caller, while a fired postcondition points it at the function itself. Crucially, the contract is a promise between your own code — it assumes the caller is also your code, bound by the same agreement and expected to honour it.

The honest traps: NDEBUG, side effects, and what assert is not

Now the part beginners get burned by, because assert() has a sharp edge built into its design. The macro is conditional: if the symbol NDEBUG is defined when you compile (typically in optimized release builds, often via `gcc -DNDEBUG -O2`), the standard says assert() expands to nothing at all. Every assertion in your program silently disappears. This is intentional — it lets you pepper your code with expensive checks during development and pay zero runtime cost in production. But it has a brutal consequence: never put code with a side effect inside an assert, because that side effect vanishes in release. The line `assert((p = malloc(8)) != NULL);` looks fine until NDEBUG is defined and the malloc() never runs.

This leads directly to the single most important rule of all: an assertion is not input validation, and you must never use it to check a recoverable error. Validating data that crossed a trust boundary from the outside world — a user's keystrokes, a network packet, a file's contents — is the subject of defensive coding, and it must use real, always-present error handling: an if-check, a return code, a graceful message. If you instead write assert(user_input_is_valid), then in a release build with NDEBUG that check is gone, and malformed input sails straight through into your program. Worse, even in a debug build, calling abort() on bad user input means a single bad packet can crash your server. Bad external input is a recoverable error, not a bug — handle it, do not assert it.

One last honesty about the relationship to undefined behavior. Assertions are a marvellous early-warning system for the bugs that cause UB — a null-pointer dereference, an out-of-bounds index, an integer that overflowed. Catching `assert(i < n)` before you write data[i] turns a silent memory corruption into a clean, named abort. But an assertion is a check, not a guarantee: it only fires for the cases you thought to write, only on the path your test actually exercised, and only in a build where it was compiled in. It narrows where bugs can hide enormously; it does not make them impossible. That harder guarantee — making whole classes of these bugs unrepresentable rather than merely checked — is the promise we weigh honestly when this ladder reaches Rust.