When applying fail fast, what is the difference between an assertion, a precondition guard, and validation of untrusted external input - and why is it dangerous to enforce a security rule with an assertion?
answer
- assertions can be compiled out
- guard = caller's bug, always on
- validation = expected, 4xx not 5xx
- never assert on user input
- assertions must be side-effect free
basics
~20 sAssertions check assumptions the developer believes are always true and are often switched off in production. Guards check what a caller must supply and always run. Input validation checks data from outside and produces a user-facing error. A security check written as an assertion can vanish in production.
solid answer
~50 sAll three fail fast, but they target different audiences. An assertion states an internal assumption that should be impossible to violate unless the code itself is buggy; many runtimes can disable assertions (Java's -ea flag off by default, C's assert removed by NDEBUG, Python's -O), so an assertion must never be the sole enforcement of anything and must be side-effect free. A precondition guard enforces the caller's contract, always runs, and throws a programmer-facing error such as IllegalArgument - the fault lies with a developer, so it should not be caught and retried. Validation of untrusted input treats bad data as an expected business case: it is always on, produces domain or field-level errors, and maps to a client error response rather than a server error. Enforcing authorisation or bounds checks with an assertion is dangerous precisely because the check may be compiled out, turning a hardened build into an unchecked one - a real class of vulnerability.
go deeper
Distinguish the three by audience: assertions for developer assumptions, guards for what a caller must pass, validation for data arriving from outside. Note that assertions can be turned off.
Add the concrete disabling mechanisms, the always-on requirement for guards and validation, the error-type and status-code mapping, and why assertions must be side-effect free.
Explain the security consequence of assertion-based checks, boundary placement so inner layers do not re-validate, allow-list validation, aggregating field errors, and the defensive-versus-offensive programming trade-off.
Argue for a system-wide trust-boundary policy: where validation lives, how validated data is represented as types, how contract violations are alerted on versus client errors that are not, and how to prevent the pattern from decaying as the codebase grows.
## Three checks that look alike and behave differently All three stop execution on a bad value, so they are easy to conflate. They differ in **who made the mistake**, **whether the check is guaranteed to run**, and **what should happen next**. ### 1. Assertion - "this should be impossible" An assertion documents an internal assumption: a loop invariant, an unreachable branch, a state that no legal sequence of calls should produce. Its audience is the developer. Key properties: - **Removable.** Many platforms strip or disable assertions in production builds: Java's `assert` only runs with the `-ea` flag, which is off by default; C/C++ `assert` is compiled away when `NDEBUG` is defined; Python's `assert` disappears under `-O`. Therefore an assertion is never a guarantee. - **Must be side-effect free.** `assert queue.pop() != null` changes behaviour depending on whether assertions are enabled - a nightmare bug class. - **Never for untrusted input.** If it can be triggered by a user, it is not an assertion; it is validation. ### 2. Precondition guard - "the caller broke the contract" A guard enforces the documented requirements of a function on its arguments or the receiver's state. Its audience is also a developer, but it **always runs**, in every build. - Signals a *programming error*: an argument out of range, a null where the contract says non-null, a method called in the wrong lifecycle state. - Should throw a distinct, specific error type (argument error, illegal state) with the parameter name and the offending value. - Callers should generally **not** catch it. Catching and retrying a programming error just hides the bug; the correct fix is in the calling code. - Placed at the top of the function, before any mutation, so failure leaves nothing half-done. ### 3. Validation of untrusted input - "bad data is expected here" At a trust boundary - HTTP request, message payload, uploaded file, config file, another team's API - malformed data is not a bug, it is a normal occurrence. Properties: - **Always on**, no exceptions, and part of the security perimeter. - Produces *domain* or *field-level* errors that are safe to show a client, and maps to a client-error status (a 4xx), never an internal-error status. - Usually **aggregates**: report all bad fields at once rather than one per round trip. - Should be **allow-list** shaped (accept only what matches the expected shape) rather than deny-list shaped. - After validation, convert the data into trusted domain types so inner layers do not re-validate. ## Why assertion-as-security-control is a genuine vulnerability Suppose an authorisation check or a bounds check is written as an assertion. In development, assertions are enabled and everything looks safe; every test passes. In production, assertions are disabled for performance - and the check simply is not there. The hardened build is the *weakest* build. The same trap applies to input length or index bounds checks: with assertions off, the code proceeds into the memory or logic the check was protecting. A second, subtler failure: because assertions may be disabled, developers get sloppy about their cost and put expensive or effectful expressions inside them, producing behaviour that differs between environments - the hardest kind of bug to reproduce. **Rule of thumb:** if a real user, a network peer, or an attacker can cause the condition to be false, it must be a guard or validation, never an assertion. ## Choosing between them Ask: *whose mistake is this, and can it happen in production?* - External actor, expected in production -> **validation**, always on, client-facing error, aggregate results. - Another developer calling my function wrongly -> **guard**, always on, programmer-facing error, do not catch. - Nobody should ever be able to cause this; it means my own logic is broken -> **assertion**, optional at runtime, side-effect free, also a documentation device. ## Related nuance - **Where to put the boundary.** Validating untrusted input in one clearly identified layer keeps inner code free of paranoia. Scattering half-validation everywhere gives neither the guarantee nor the simplicity. - **Defensive versus offensive programming.** Defensive code tries to survive bad input (substituting defaults, clamping values). Offensive programming - the fail-fast stance - deliberately refuses, on the grounds that a silently clamped value produces wrong answers that nobody will investigate. Use defensive substitution only where the fallback is genuinely correct behaviour, not to hide a defect. - **Static enforcement beats all three.** A non-null type or a range-checked value type removes the whole question by making the illegal call fail at compile time.
- Should a precondition guard failure be reported to the client as a 400 or a 500?A guard failure means an internal caller broke a contract - a bug in your own code - so it surfaces as a 500 and an alert. Malformed data from an external client is validation and surfaces as a 400. Mapping guard failures to 400 hides real defects behind what looks like user error.
- Is it acceptable for an assertion to call a method that mutates state?No. Because assertions can be disabled, any side effect inside one makes program behaviour depend on the build flags, producing bugs that only appear in one environment. Assertions must be pure predicates.
- How do you avoid re-validating the same value in every layer?Validate once at the trust boundary and parse the raw data into a domain type that can only hold valid values. Inner layers take the type as a parameter and simply trust it; the guarantee is carried structurally rather than re-checked.
saying these in an interview costs you the question
- Using assertions to enforce authorisation, bounds, or any rule an attacker can influence - the check disappears when assertions are disabled.
- Putting side effects inside an assertion, so behaviour differs between development and production builds.
- Returning a 500 for malformed client input, or a 400 for an internal contract violation, blurring who is at fault.
- Catching an argument-error guard failure and retrying, which hides a caller bug instead of fixing it.
- Silently clamping or defaulting invalid values ("defensive programming") so wrong results are produced with no signal at all.