Contract checks cost cycles, and several languages let you compile them out — Eiffel per class and per assertion level, Rust's debug_assert!, Java's assert being inert unless -ea is passed. How would you decide which checks may be stripped in a production build, and where is stripping never acceptable?
answer
- Contract = only a bug can trigger it; validation = the world can
- Java assert inert without -ea, hence explicit throw in real APIs
- Eiffel: keep preconditions on, drop invariants first
- Rust: assert! stays, debug_assert! goes, types cannot be stripped
- Racket wraps values — stripping changes semantics, not just speed
basics
~20 sStrip checks that catch your own team's bugs; never strip a check standing between untrusted input and a decision — that is validation, not a contract. Eiffel strips per class and level, Rust's debug_assert! vanishes under --release while assert! stays, Java's assert is inert by default, and SPARK proves contracts so nothing runs at all.
solid answer
~60 sDecide by *who* the check protects against, then note that each language pushes you to a different default. - **Eiffel** — assertion monitoring is set per class and per level (preconditions only, all, none). The classic production setting is preconditions on: your own postconditions catch your bugs, which testing already exercises, but a caller's bug is the one you cannot see. - **Java** — `assert` is inert unless `-ea` is passed, so an assertion-expressed contract silently disappears in production. That is precisely why Java APIs hand-write `if (x == null) throw new IllegalArgumentException(...)`: the contract has to be re-expressed as ordinary code to survive. - **Rust** — `debug_assert!` is compiled out under `--release`, `assert!` is not, and a precondition moved into a type such as `NonZeroUsize` cannot be stripped at all. - **Ada/SPARK** — contracts are discharged by proof at build time: zero runtime cost and no violation possible. - **Racket** — boundary contracts stay on, because blame is the deliverable; the cost is paid only at module crossings. Stripping changes semantics, not just speed.
go deeper
Know that some assertion mechanisms are disabled in production builds, and that user input must be validated with code that always runs.
Distinguish contract from validation with a concrete test, and name one always-on and one strippable construct in a language you use.
Justify the asymmetry (preconditions on, invariants off first), and know that Java's assert defaults off, which is why real APIs throw explicitly.
Set the policy: which categories ship enabled, how a fleet-wide rollout goes through an observe phase before enforcement, and when to discharge an obligation in types or by proof so that no build flag can change behaviour.
## The decision, stated once Ask what a failed check protects against. - If only a defect in code you control can produce the condition, it is a **contract**. It may be compiled out, because in a correct program it never fires, and because its purpose is to shorten the distance between a bug and its symptom. - If a well-behaved outside world can produce the condition — a request body, a file, a message from another service, a value from a plugin — it is **validation**. It must survive every build, because in production it *will* fire and the program must answer correctly rather than continue. Everything else is calibration. The uncomfortable part is that several languages make the wrong thing easy. ## Java: the check that silently isn't there Java's `assert` statement is disabled unless the JVM is started with `-ea`. Since almost nobody enables assertions in production, a precondition written as `assert amount > 0` is documentation with a test-time side effect. This single decision explains a visible style difference: mature Java APIs express preconditions as ordinary code — `if (amount <= 0) throw new IllegalArgumentException(...)`, or `Objects.requireNonNull(x)` — because that is the only formulation that survives. Kotlin makes the same choice explicit in its library: `require` and `check` are always-on functions, while `assert` is routed through the JVM's `-ea` flag. So in the same ecosystem you have two mechanisms with opposite lifetimes, and picking the wrong one is invisible until a production incident. ## Eiffel: levels, per class Eiffel treats monitoring as a build-time policy with granularity: assertions can be enabled per class and per category — preconditions only, preconditions and postconditions, all including invariants and loop variants, or none. The received wisdom is asymmetric and worth being able to justify: keep **preconditions** on in the field, relax postconditions and invariants first. Your postconditions catch your own defects, which your test suite already exercises against your own code; your preconditions catch *callers'* defects, and you cannot test the code you did not write. The cost profile agrees — invariants are evaluated on every qualified call and are usually the expensive ones. ## Rust: two macros and a third option Rust splits the mechanism by name. `assert!` and `assert_eq!` remain in release builds; `debug_assert!` is compiled out under `--release`. The convention that follows is direct: `assert!` for anything guarding memory safety or a decision the program cannot back out of, `debug_assert!` for expensive internal consistency checks. But Rust's real answer is the third option — encode the precondition in a type (`NonZeroUsize`, a validated newtype) so there is no check to strip and no build flag that can change behaviour. That is the only approach in this list where the production and debug builds are provably equivalent with respect to the obligation. ## Ada and SPARK: discharge instead of check Ada 2012 contracts can be checked at runtime; the SPARK subset instead *proves* them at build time with a verification condition generator. A discharged precondition costs nothing at runtime and cannot be violated at all, because no execution path reaching that call can produce a violating state. This is the high end of the spectrum and the reason contracts are taken seriously in avionics and rail: the trade is a restricted language subset and proof effort, not cycles. ## Racket: contracts you must not strip Racket sits at the opposite pole. Boundary contracts are the product, not an optional check: a higher-order contract *wraps* the value it guards so that blame can be attributed when the wrapped function is eventually applied. Removing it does not merely skip a comparison, it removes a wrapper and changes what the value is. That is the general lesson: for flat boolean assertions, stripping changes performance; for wrapping or proxying contract systems, stripping changes semantics. ## C++: the mode is part of the build C++ contracts were pulled from C++20 and standardised for C++26 with explicit *evaluation semantics* — broadly, ignore, observe (report and continue), and enforce (report and terminate) — selectable at translation time. Making 'report and continue' a first-class mode is an admission that a large codebase cannot flip from unchecked to terminate-on-violation in one step: you turn on observation, collect violations from real traffic, fix them, then enforce. It is a rollout strategy encoded in the language, and it is the right template even in languages without the feature — ship the check in log-only mode first. ## The rules I would actually write down 1. Preconditions on public API boundaries: always on, expressed in always-on constructs (Kotlin `require`, Rust `assert!`, an explicit `throw`), never as a strippable assert. 2. Untrusted input: never a contract. It gets ordinary validation code and a defined error response. 3. Expensive internal consistency checks over data structures: strippable, and default off in release (`debug_assert!`, Eiffel's invariant level). 4. When introducing checks into a running system, run them in observe/log mode first and enforce afterwards. 5. Prefer discharging the obligation — a type, a proof, an immutable construction path — over any check whose presence depends on a build flag.
- Why is the usual advice to keep preconditions enabled in production while relaxing postconditions and invariants first?Postconditions and invariants catch your own defects, and your test suite already exercises your own code against them. Preconditions catch callers' defects, including callers you did not write and cannot test. The cost profile points the same way: invariants are evaluated around every externally visible call and are typically the most expensive category, while a precondition is usually a cheap check at an entry point you were about to pay for anyway.
- Why does removing a Racket boundary contract differ in kind from removing a boolean assert?A flat assert evaluates a predicate and either continues or aborts, so removing it changes timing and safety-net coverage but not the values flowing through. A higher-order Racket contract installs a wrapper around the guarded value so that later applications can be checked and blamed. Removing it removes the wrapper, so the identity and behaviour of the value change. For any proxying or wrapping contract system, stripping is a semantic change, not an optimisation.
saying these in an interview costs you the question
- Expressing a public API precondition with Java's assert and assuming it runs in production
- Treating request or file validation as a contract that may be compiled out
- Assuming stripping a check is always purely a performance decision
- Enabling full contract checking on a large legacy codebase in enforce mode as the first step, with no observe phase
- Believing a proof-based system like SPARK adds runtime cost, or that stripping Rust's assert! and debug_assert! are the same thing