Skip to content

Introduce Assertion

A piece of code often only works when something is already true — a discount is between 0 and 1, a list is non-empty, an account has been opened. That assumption usually lives in the programmer’s head, not in the code. Introduce Assertion writes the assumption down as a check that does nothing when the world is as expected and stops the program loudly when it is not.

An assertion is a statement about what must be true at a point in the code. It is not error handling for input you expect to be wrong; it is a tripwire for a situation that should be impossible. If it ever fires, a caller has a bug.

You read a calculation and think, “this only makes sense if the rate is positive” — but nothing in the code says so. The assumption is invisible, so the day a zero or a negative slips through, the function happily produces garbage. The damage surfaces three layers away, in a total that is silently wrong, and you spend an afternoon tracing it back. The condition the code relied on was never stated, so nothing guarded it.

A function applies a discount that must be a fraction between 0 and 1. Before, that constraint is unwritten. After, an assertion states it, so a bad rate fails at the door.

// Before
function applyDiscount(price: number, rate: number): number {
// silently assumes 0 <= rate <= 1
return price - price * rate;
}
// After
function applyDiscount(price: number, rate: number): number {
if (rate < 0 || rate > 1) {
throw new Error(`discount rate must be in [0, 1], got ${rate}`);
}
return price - price * rate;
}
flowchart LR
  A["Caller passes bad value"] --> B{"Assertion present?"}
  B -->|"No"| C["Wrong math runs<br/>NaN spreads<br/>fails far away"]
  B -->|"Yes"| D["Stops here<br/>with a clear message"]
An assertion stops a broken assumption at its source
  1. Find the assumption. Read the suspicious code and finish the sentence “this only works if…”. That clause is your assertion.
  2. Express it as a check that is true in the normal case. Place it as close as possible to the code that depends on it, before the work begins.
  3. Make the failure loud and specific: include the offending value in the message so a future reader sees what broke the assumption, not just that it broke.
  4. Run your tests. Healthy paths must keep passing untouched — a correct assertion changes nothing for valid input.
  5. Add a test that feeds an illegal value and confirm the assertion fires. This proves the tripwire is wired up.
  6. Repeat for the next unstated assumption. Resist asserting things that are obviously guaranteed by types; assert what a caller could realistically get wrong.

Introduce an assertion when a section of code quietly depends on a condition and a violation would otherwise corrupt data far from the cause. Assertions double as documentation: they tell the next reader exactly what the code expects, and unlike a comment they cannot drift out of sync, because a false one fails.

The line to watch is assertion versus error handling. An assertion guards against a programmer mistake — a state that should be impossible if every caller is correct. Input that can legitimately be malformed (a user typo, a bad network payload) is not an assertion’s job; validate it and return a real error. In Rust this is the assert!/debug_assert! versus Result choice; in Go it is panic versus a returned error. Never put a side effect inside an assertion, since builds that strip assertions (Python’s -O, Rust’s debug_assert!) would then silently change behaviour.

What is an assertion meant to catch?
Since Go has no assert keyword, how does the lesson state an invariant for a true "cannot happen" bug?
In Rust, which macro runs the check only in development builds?
Why should an assertion never contain a side effect?