Loading learning content...
Every time you call a method, you're entering into a contract. This contract isn't written in a legal document or even necessarily in documentation—it's embedded in the assumptions the method makes about the world when it begins execution.
A precondition is the caller's side of this contract: the promises the caller must keep for the method to work correctly. When you call divide(a, b), you're implicitly promising that b ≠ 0. When you call withdraw(amount), you're promising amount > 0. When you pass an array to binarySearch(arr, target), you're promising that arr is already sorted.
These promises aren't arbitrary bureaucracy—they're correctness guarantees. A method can only guarantee correct behavior if its caller upholds the precondition. Break the precondition, and the method is free to do anything: return garbage, throw an exception, corrupt state, or even hang indefinitely.
Understanding preconditions is essential for the Liskov Substitution Principle because how a subclass handles preconditions determines whether it can safely replace its parent. This page explores preconditions in depth: what they are, why they matter, and the precise rules governing their use in inheritance hierarchies.
By the end of this page, you will: • Understand preconditions as formal contracts that callers must satisfy • Recognize preconditions in real-world code (explicit and implicit) • Master the LSP rule: subclasses may weaken but never strengthen preconditions • Know how to design, document, and verify preconditions in your code • Understand the relationship between preconditions, defensive programming, and robust design
A precondition is a logical assertion that must be true at the moment a method begins execution for the method to behave correctly. It defines the valid input space—the set of conditions under which the method's contract applies.
Preconditions answer the question: "What must be true for me to do my job correctly?"
Consider a simple example:
1234567891011121314151617181920212223
class BankAccount { private balance: number; /** * Withdraws the specified amount from the account. * * @param amount The amount to withdraw * @precondition amount > 0 * @precondition amount <= this.balance * @postcondition this.balance = old(this.balance) - amount */ withdraw(amount: number): void { // Precondition check (defensive, not required by contract) if (amount <= 0) { throw new Error("Amount must be positive"); } if (amount > this.balance) { throw new Error("Insufficient funds"); } this.balance -= amount; }}This withdraw method has two preconditions:
amount > 0: You cannot withdraw zero or negative amountsamount <= balance: You cannot withdraw more than you haveIf the caller violates these preconditions, the method is not obligated to work correctly. The defensive checks we've added are a courtesy—a way to fail early with clear error messages. But under strict Design by Contract, if a precondition is violated, the method could just as validly corrupt the balance or throw an obscure internal exception.
There's an important distinction between preconditions and input validation:
Precondition: A formal contract—violation is the caller's fault, and the method makes no guarantees.
Input Validation: Defensive code that handles bad input gracefully, often for untrusted sources.
In practice, most code uses a hybrid: treating documented preconditions as soft contracts while adding defensive checks for safety. The key is consistency—document what callers must guarantee vs. what your method will check.
Preconditions can involve various aspects of program state. Understanding what can be constrained in a precondition helps you recognize and design better contracts.
| Category | Description | Example |
|---|---|---|
| Parameter Constraints | Restrictions on method arguments | index >= 0 && index < array.length |
| Object State | Requirements on the object's internal state | this.isInitialized == true |
| External State | Assumptions about the external environment | database.isConnected == true |
| Ordering Constraints | Assumptions about call sequence | open() called before read() |
| Type Constraints | Requirements beyond static types | list is sorted in ascending order |
| Relationship Constraints | Requirements involving multiple parameters | startIndex < endIndex |
Let's examine each category with realistic examples:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
// ═══════════════════════════════════════════════════════════════// PARAMETER CONSTRAINTS// Restrictions on method arguments// ═══════════════════════════════════════════════════════════════ class ArrayList<T> { private items: T[] = []; /** * @precondition index >= 0 && index < this.size() */ get(index: number): T { // Caller promises: valid index range return this.items[index]; } /** * @precondition fromIndex >= 0 && toIndex <= this.size() * @precondition fromIndex <= toIndex */ subList(fromIndex: number, toIndex: number): T[] { // Caller promises: valid range, proper ordering return this.items.slice(fromIndex, toIndex); }} // ═══════════════════════════════════════════════════════════════// OBJECT STATE CONSTRAINTS // Requirements on the object's internal state// ═══════════════════════════════════════════════════════════════ class FileWriter { private handle: FileHandle | null = null; /** * @precondition this.isOpen() == true */ write(data: string): void { // Caller promises: file was opened first this.handle!.write(data); } /** * @precondition this.isOpen() == false */ open(path: string): void { // Caller promises: not already open (prevents resource leak) this.handle = fs.openSync(path, 'w'); }} // ═══════════════════════════════════════════════════════════════// ORDERING/PROTOCOL CONSTRAINTS// Assumptions about method call sequences// ═══════════════════════════════════════════════════════════════ class Transaction { private isStarted = false; private isCommitted = false; /** * @precondition !this.isStarted && !this.isCommitted */ begin(): void { // Can only begin once, before any commit this.isStarted = true; } /** * @precondition this.isStarted && !this.isCommitted */ commit(): void { // Must have begun, cannot commit twice this.isCommitted = true; } /** * @precondition this.isStarted && !this.isCommitted */ rollback(): void { // Must have begun, cannot rollback after commit this.isStarted = false; }} // ═══════════════════════════════════════════════════════════════// SEMANTIC TYPE CONSTRAINTS// Requirements beyond what the type system can express// ═══════════════════════════════════════════════════════════════ class SearchAlgorithms { /** * @precondition arr is sorted in ascending order * @precondition arr contains no duplicate elements */ binarySearch<T>(arr: T[], target: T): number { // Caller promises: array is sorted (runtime cannot check efficiently) let low = 0, high = arr.length - 1; while (low <= high) { const mid = Math.floor((low + high) / 2); if (arr[mid] === target) return mid; if (arr[mid] < target) low = mid + 1; else high = mid - 1; } return -1; }}Preconditions like "array is sorted" or "graph is acyclic" are called semantic preconditions because they describe properties the type system cannot encode. They're especially dangerous because:
Document semantic preconditions meticulously. They are where bugs hide.
Now we reach the heart of preconditions in LSP. When a subclass overrides a method, how must it handle the parent's preconditions?
The rule is deceptively simple but critically important:
A subclass may weaken (loosen) preconditions but must never strengthen (tighten) them.
This means a subclass can accept a wider range of inputs than the parent, but never a narrower range. Let's understand why with a concrete analysis.
If S is a subtype of T, and method m in T has precondition P_T, then the overriding method m in S must have a precondition P_S such that:
P_T ⟹ P_S (P_T implies P_S)
In other words: anything that satisfies the parent's precondition must also satisfy the child's precondition. The child can accept more, but never less.
Why does strengthening preconditions break LSP?
Consider this scenario:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// ═══════════════════════════════════════════════════════════════// BASE CLASS: General payment processor// ═══════════════════════════════════════════════════════════════ class PaymentProcessor { /** * Processes a payment of the given amount. * * @precondition amount > 0 */ processPayment(amount: number): PaymentResult { // Accepts any positive amount return this.executePayment(amount); } protected executePayment(amount: number): PaymentResult { // Base implementation return { success: true, transactionId: generateId() }; }} // ═══════════════════════════════════════════════════════════════// ❌ LSP VIOLATION: Strengthened precondition// ═══════════════════════════════════════════════════════════════ class PremiumPaymentProcessor extends PaymentProcessor { /** * Processes a payment of the given amount. * * @precondition amount > 0 * @precondition amount >= 100 // ⚠️ NEW, STRONGER PRECONDITION! */ processPayment(amount: number): PaymentResult { if (amount < 100) { // Rejects amounts the parent would accept! throw new Error("Minimum payment is $100"); } return super.processPayment(amount); }} // ═══════════════════════════════════════════════════════════════// Client code written against the base class// ═══════════════════════════════════════════════════════════════ function processMicroPayments(processor: PaymentProcessor): void { // These are all valid according to PaymentProcessor's contract const amounts = [0.99, 5.00, 25.00, 50.00]; for (const amount of amounts) { // ✅ All amounts > 0, so precondition is satisfied const result = processor.processPayment(amount); console.log(`Processed $${amount}: ${result.success}`); }} // ═══════════════════════════════════════════════════════════════// The substitution problem// ═══════════════════════════════════════════════════════════════ // This works perfectly:const standard = new PaymentProcessor();processMicroPayments(standard); // ✅ All payments succeed // This breaks the code:const premium = new PremiumPaymentProcessor();processMicroPayments(premium); // ❌ Throws on $0.99, $5.00, $25.00, $50.00 // The client code is CORRECT according to PaymentProcessor's contract,// but PremiumPaymentProcessor broke that contract by adding requirements.The violation is clear: Client code written to the PaymentProcessor contract passes amounts like $5.00, which satisfy amount > 0. But PremiumPaymentProcessor adds an extra requirement (amount >= 100) that the client knows nothing about. Substitution fails.
Now let's see the correct approach—weakening preconditions:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// ═══════════════════════════════════════════════════════════════// BASE CLASS: Requires positive amounts// ═══════════════════════════════════════════════════════════════ class StrictCalculator { /** * Computes the square root of n. * * @precondition n > 0 (strictly positive) */ squareRoot(n: number): number { // Doesn't handle zero or negatives return Math.sqrt(n); }} // ═══════════════════════════════════════════════════════════════// ✅ LSP COMPLIANT: Weakened precondition// ═══════════════════════════════════════════════════════════════ class FlexibleCalculator extends StrictCalculator { /** * Computes the square root of n. * * @precondition n >= 0 (zero is now acceptable!) */ squareRoot(n: number): number { // Accepts everything the parent accepts, plus zero if (n === 0) return 0; // Handle the new case return super.squareRoot(n); }} // ═══════════════════════════════════════════════════════════════// Even more flexible subclass// ═══════════════════════════════════════════════════════════════ class ComplexCalculator extends FlexibleCalculator { /** * Computes the square root of n (returns complex for negatives). * * @precondition true (accepts any real number!) */ squareRoot(n: number): number | { real: number; imaginary: number } { if (n < 0) { // Handle negatives by returning complex result return { real: 0, imaginary: Math.sqrt(-n) }; } return super.squareRoot(n); }} // ═══════════════════════════════════════════════════════════════// All substitutions work correctly// ═══════════════════════════════════════════════════════════════ function calculateRoots(calc: StrictCalculator): void { // Only uses inputs valid for StrictCalculator const values = [1, 4, 9, 16, 25]; for (const v of values) { console.log(`√${v} = ${calc.squareRoot(v)}`); }} // All of these work:calculateRoots(new StrictCalculator()); // ✅ OriginalcalculateRoots(new FlexibleCalculator()); // ✅ Accepts everything parent acceptscalculateRoots(new ComplexCalculator()); // ✅ Accepts even more // Subclasses accept MORE inputs, never fewer.// They never surprise client code with rejected inputs.When preconditions are properly weakened (or kept the same): • Any input the client passes to the parent will also be accepted by the child • Client code never sees unexpected rejections • Substitution is always safe
The subclass becomes a "drop-in replacement" that might be more capable, but is never more restrictive.
The precondition rule becomes intuitive when visualized. Think of preconditions as defining "acceptable input regions." Let's visualize this:
The geometry is simple:
Clients write code that sends inputs within the parent's acceptable region. If a subclass shrinks that region, some of those inputs will be rejected—breaking the contract.
n > 0 → n >= 0 (now accepts zero)arr.length > 0 → true (accepts empty arrays)age >= 18 → age >= 0 (accepts younger)amount <= 1000 → amount <= 10000 (higher limit)user != null → true (handles null)n >= 0 → n > 0 (rejects zero)true → arr.length > 0 (rejects empty)age >= 0 → age >= 18 (rejects younger)amount <= 10000 → amount <= 100 (lower limit)true → user != null (rejects null)Let's examine precondition patterns you'll encounter in real systems, along with their proper handling in subclasses.
Collections have rich preconditions:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
interface List<T> { /** * @precondition index >= 0 && index < size() */ get(index: number): T; /** * @precondition index >= 0 && index <= size() // Note: <= for insert */ insert(index: number, element: T): void; /** * @precondition !isEmpty() */ removeFirst(): T;} // ═══════════════════════════════════════════════════════════════// Subclass that handles boundary cases more gracefully// ═══════════════════════════════════════════════════════════════ class SafeList<T> implements List<T> { private items: T[] = []; /** * @precondition true // Weakened! Now handles any index */ get(index: number): T | undefined { // Returns undefined for out-of-bounds instead of throwing return this.items[index]; } /** * @precondition true // Weakened! Clamps to valid range */ insert(index: number, element: T): void { const clampedIndex = Math.max(0, Math.min(index, this.items.length)); this.items.splice(clampedIndex, 0, element); } /** * @precondition true // Weakened! Returns undefined if empty */ removeFirst(): T | undefined { return this.items.shift(); }}Strengthened preconditions often appear as innocent-looking guard clauses. Here's how to spot them:
if (x < threshold) throw Error, it's likely strengtheninginit() first" when parent didn't require it12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// ═══════════════════════════════════════════════════════════════// PATTERN 1: The "Innocent" Guard Clause// ═══════════════════════════════════════════════════════════════ class Logger { log(message: string, level: LogLevel = "INFO"): void { // Accepts any message at any level this.writeLog(message, level); }} class ProductionLogger extends Logger { log(message: string, level: LogLevel = "INFO"): void { // ⚠️ RED FLAG: New conditional at start of override if (level === "DEBUG" || level === "TRACE") { return; // Silently ignores valid inputs — still a violation! } super.log(message, level); }}// This is subtle: it doesn't throw, but it IGNORES inputs the parent would handle.// A client logging DEBUG messages gets unexpected behavior. // ═══════════════════════════════════════════════════════════════// PATTERN 2: The Null Check Addition// ═══════════════════════════════════════════════════════════════ class UserRepository { findById(id: string | null): User | null { if (id === null) return null; // Explicitly handles null return this.database.find(id); }} class CachingUserRepository extends UserRepository { findById(id: string | null): User | null { // ⚠️ RED FLAG: Parent handles null, child rejects it if (id === null) { throw new Error("ID cannot be null"); // ❌ LSP Violation! } return this.cache.get(id) ?? super.findById(id); }} // ═══════════════════════════════════════════════════════════════// PATTERN 3: The Hidden State Requirement// ═══════════════════════════════════════════════════════════════ class Connection { send(data: Buffer): void { // Sends data (opens connection lazily if needed) if (!this.isConnected) this.connect(); this.socket.write(data); }} class SecureConnection extends Connection { private tlsEstablished = false; send(data: Buffer): void { // ⚠️ RED FLAG: New state requirement not in parent if (!this.tlsEstablished) { throw new Error("TLS handshake not completed"); // ❌ } super.send(data); } // The fix: establish TLS automatically in send() sendSecure(data: Buffer): void { if (!this.tlsEstablished) this.establishTLS(); super.send(data); }}When reviewing an override, ask: "Is there ANY input that the parent would accept that this child would reject?" If yes, preconditions are strengthened, and LSP is violated.
Run through the parent's documented (and implied) valid input space. Every valid input to the parent must be valid to the child.
A common source of confusion: Doesn't adding precondition checks at runtime (defensive programming) violate client trust?
The answer is nuanced. Defensive checks and preconditions serve different purposes:
| Aspect | Precondition (Contract) | Defensive Check |
|---|---|---|
| Purpose | Define what caller must guarantee | Catch mistakes early, improve debugging |
| Who is responsible? | Caller (precondition violation = caller bug) | Method (defensive methods are more robust) |
| In optimized builds? | Often removed (assertions) | Usually kept (safety net) |
| When violated? | Undefined behavior (anything can happen) | Defined behavior (specific exception) |
| Documentation | Part of the API contract | Implementation detail |
Best practice: Use both, clearly distinguished:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
class Vector<T> { private elements: T[]; /** * Gets the element at the specified index. * * CONTRACT (Precondition): * index >= 0 && index < this.size() * * The caller is responsible for ensuring valid indices. * If precondition is violated, behavior is undefined. * * In practice, we throw to fail fast, but this is a * courtesy — not a guarantee the API promises. */ get(index: number): T { // ═══════════════════════════════════════════════════════ // ASSERTION: Precondition check (may be stripped in prod) // This documents and enforces the contract during development // ═══════════════════════════════════════════════════════ console.assert( index >= 0 && index < this.elements.length, `Precondition violated: index ${index} out of bounds [0, ${this.elements.length})` ); // ═══════════════════════════════════════════════════════ // DEFENSIVE CHECK: Kept in production for safety // This converts undefined behavior into a clear exception // ═══════════════════════════════════════════════════════ if (index < 0 || index >= this.elements.length) { throw new RangeError( `Index ${index} out of bounds for vector of size ${this.elements.length}` ); } return this.elements[index]; } /** * Returns element at index, or undefined if out of bounds. * * CONTRACT (Precondition): true (none!) * * This method has NO preconditions — it handles all inputs. * Use this when you cannot guarantee valid indices. */ tryGet(index: number): T | undefined { // No assertions needed — any input is valid if (index < 0 || index >= this.elements.length) { return undefined; // Part of the contract, not a violation } return this.elements[index]; }} // ═══════════════════════════════════════════════════════════════// Usage guidance// ═══════════════════════════════════════════════════════════════ // KNOWN VALID INDEX: Use get() for clear semanticsconst element = vector.get(knownValidIndex); // UNCERTAIN INDEX: Use tryGet() to handle gracefullyconst maybeElement = vector.tryGet(userProvidedIndex);if (maybeElement !== undefined) { // proceed}Preconditions define the contract. Defensive checks enforce it.
A well-designed API offers both: • Strict methods with preconditions (fast path, caller ensures validity) • Safe methods without preconditions (handle all inputs, return errors/optionals)
Let the caller choose based on their context.
We've thoroughly explored preconditions as the first half of Design by Contract. Let's consolidate the key insights:
What's next:
Preconditions are the caller's responsibility — what they must guarantee. But contracts are two-sided. The next page explores postconditions: what the method promises to deliver when preconditions are met. In the LSP world, postconditions follow the opposite rule: subclasses may strengthen but never weaken them.
You now understand preconditions as formal contracts and can recognize LSP violations from strengthened preconditions. This knowledge is essential for designing robust inheritance hierarchies where substitution is always safe.