A specification written in a comment is a wish; a specification written as an assert is a
wish the machine will check. An assertion is a boolean expression you plant in the code
with the claim "at this point in every execution, this is true" — and if it is ever false, the program
stops and tells you, at the scene of the crime, instead of limping on and corrupting something three
function calls later. Assertions are how the pre/postconditions of the
The most powerful assertion is not attached to a single line but to an entire object: an invariant — a property that holds every time you look. This page is about the two together: assertions as executable specifications, and the invariants (data, representation, class) that keep a data structure honest between operations.
The precondition of a routine becomes an assertion at its entry; the postcondition becomes an assertion at its exit. An assertion that guards entry is defensive programming: rather than trust callers, the routine checks the caller's obligation itself and refuses to proceed on garbage. The governing philosophy is fail-fast — detect the violated assumption at the earliest possible instant, because the distance (in time and stack frames) between a corruption and its symptom is exactly the cost of debugging it.
A crucial distinction: an assertion is not error handling. Error handling copes with situations
that can legitimately happen at runtime — a missing file, a malformed request — and the program
must recover gracefully. An assertion documents something the programmer believes cannot happen
if the code is correct; if it fires, the code has a bug, and the right response is to abort, not
recover. Put the other way: never validate untrusted external input with an assertion (which may be
compiled out), and never try/catch around a logic bug. Assertions police internal
consistency; exceptions handle external reality.
An invariant is an assertion attached to a scope larger than a single statement. Three flavours matter, from the concrete outward:
The class invariant is the load-bearing idea. It lets you reason locally: to trust a method, you assume the invariant on entry (the object was left valid by whatever ran before) and you must re-establish it on exit. Every public method is thus a little Hoare triple whose pre- and postconditions both include the invariant. The invariant is the glue that makes an object more than a bag of fields.
A fixed-capacity stack stores its elements in an array
Two things to notice. First, the preconditions of push (not full) and
pop (not empty) are the caller's obligation — the class offers isFull/
isEmpty so callers can discharge them. Second, checkInvariant() is called after
every mutation, so the class invariant is machine-checked at every public boundary. Because
data and size are private, no outside code can put the object into a
state that violates the invariant — encapsulation is what makes an invariant enforceable.
Expose the fields and the invariant becomes a wish again.
The trace below follows push on a stack with capacity 3 currently holding 2 elements. The
invariant
| Step | Invariant | |
|---|---|---|
on entry to push | 2 | holds |
| precondition | 2 | holds |
data[2] = x | 2 | holds |
size = size + 1 | 3 | holds ( |
on exit (checkInvariant) | 3 | holds |
Had the precondition push at data[3]
out of bounds and bump
Everything here is an invariant over a scope in space — a data structure, an object. There is a
second, equally important kind that is an invariant over a scope in time: a
In many languages assertions are disabled in production for speed: Java runs without
-ea by default, C's assert vanishes under NDEBUG, and a stripped
build may drop console.assert too. So the golden rule: an assertion's expression must
have no side effects. The catastrophe is code like assert(stack.pop() === 5) — in a
debug build it pops and checks; in a release build the whole line disappears, the pop never
happens, and the program behaves differently depending on a compiler flag. That is one of the nastiest
bugs there is, because it cannot be reproduced in the debugger where assertions are on. Assertions are for
checking state, never for changing it. Two corollaries: never use an assertion to
validate untrusted external input (it may be compiled away, leaving the input unchecked), and never rely on
an assertion's side effect for correctness — if the work must happen, do it on a normal line.