SECTION 1
Historical Context & Motivation
Imagine handing a recipe to a friend but forgetting to mention that the oven must be preheated first. The dish fails, and nobody knows why. In software development, functions face the same problem: without a clear description of what goes in and what comes out, bugs hide everywhere. The idea of a function contract — a precise statement of a function's expectations and promises — grew out of decades of painful experience with unreliable code. By studying how this concept evolved, you will understand why modern JavaScript developers treat contracts and invariants as essential communication tools.
1969 • Hoare Logic
1986 • Design by Contract
1995 • JavaScript Is Born
2012 • TypeScript & JSDoc Contracts
2020s • Runtime Validation Libraries
Throughout this history, one question kept surfacing: How do we make sure the caller and the function agree on what is acceptable input and what the function promises to deliver? That is exactly the question function contracts and invariants answer.
SECTION 2
Core Principles & Definitions
A function contract is like a legal agreement between two parties: the caller (the code that invokes the function) and the function itself. The caller promises to supply valid inputs, and the function promises to return a correct result. If either party breaks the deal, a bug is born. On top of individual contracts, an invariant is a condition that must always remain true — before, during, and after the function runs.
Precondition (Inputs)
Postcondition (Outputs)
Invariant
Side Effects
KEY TAKEAWAY
SECTION 3
Visual Explanation — Anatomy of a Function Contract
Notice how the arrows flow from left to right, mirroring the way data travels through a function call. The preconditions act as a gatekeeper: if the caller passes an invalid amount (say, a negative number), the contract is already broken before the function body even runs. The postconditions are the function's promise back to the caller. And the invariant wraps around everything like a safety net, ensuring the system stays in a valid state no matter how many times the function is called.
SECTION 4
How Contracts Work in JavaScript
JavaScript does not have built-in contract syntax the way Eiffel does, so developers express contracts through a combination of documentation comments, guard clauses (early validation at the top of a function), and assertions that throw errors when a contract is violated. Let's look at each technique.
Technique 1 — JSDoc Contract Comments
A JSDoc comment sits directly above a function and describes its contract in a structured way. The @param tag describes each input, the @returns tag describes the output, and the @throws tag lists what errors may occur if the contract is violated. Here is an example:
JSDOC CONTRACT PATTERN
The @param tag states the precondition. The @returns tag states the postcondition. The @invariant tag (custom) documents what must remain true.
Technique 2 — Guard Clauses (Runtime Enforcement)
A guard clause is an if statement at the very top of a function that checks the precondition and throws an error immediately if it fails. This converts a silent bug into a loud, obvious error message. Think of it as a bouncer at the door who checks IDs before letting anyone into the club.
GUARD CLAUSE PATTERN
Each if block validates one part of the precondition. If any check fails, the function exits immediately with a descriptive error.
Technique 3 — Postcondition Assertions
You can also check postconditions and invariants at the end of a function using console.assert() or a custom assert helper. If the function's own output violates its contract, the assertion catches the mistake during development. This is particularly useful for verifying invariants: right before returning, you assert that the invariant still holds.
POSTCONDITION ASSERTION PATTERN
The console.assert call checks the invariant. If newBalance is negative, it logs an error to the console, alerting the developer.
SECTION 5
Types of Contracts & Invariants
Not all contracts are the same. Some focus on the shape of data (type contracts), while others focus on the meaning or range of values (semantic contracts). Understanding the different categories helps you write more precise and useful documentation.
| Contract Type | Example in JavaScript | What It Prevents |
|---|---|---|
| Type (primitive) | typeof x === 'number' | Passing a string where a number is expected |
| Type (shape) | 'name' in user && 'age' in user | Missing required properties on an object |
| Semantic (range) | age >= 0 && age <= 150 | Nonsensical values like a negative age |
| Semantic (relationship) | startDate < endDate | An event ending before it starts |
| Invariant | items.length === uniqueCount | Duplicate entries corrupting a set |
Common contract types with JavaScript examples
SECTION 6
Worked Example — Building a Contract for a Shopping Cart
Let's walk through a realistic example. You are building an addItem function for a shopping cart. We will define its full contract — preconditions, postconditions, invariant — and then write the JavaScript code that enforces it.
Writing a Function Contract for addItem()
- 1
Step 1 — Identify the Inputs (Preconditions)
The function takes two arguments:item(an object withnameandpriceproperties) andquantity(a positive integer). The preconditions are:item.namemust be a non-empty string,item.pricemust be a number greater than zero, andquantitymust be a whole number ≥ 1.Preconditions:typeof item.name === 'string' && item.name.length > 0 && item.price > 0 && Number.isInteger(quantity) && quantity >= 1 - 2
Step 2 — Identify the Output (Postcondition)
After a successful call, the function returns the updated cart array. The postcondition guarantees that the returned array contains the new item with the correct quantity, and the cart's total price has increased by exactlyitem.price × quantity.Postcondition:newTotal === oldTotal + item.price × quantity - 3
Step 3 — State the Invariant
No matter how many items are added or removed, the cart'stotalproperty must always equal the sum ofprice × quantityfor every item in the cart. This is the data invariant for the cart.Invariant:cart.total === cart.items.reduce((sum, i) => sum + i.price * i.qty, 0) - 4
Step 4 — Write the JSDoc Comment
Now we combine everything into a JSDoc comment block that future developers (including future you!) can read:/** * Adds an item to the shopping cart. * @param {{name: string, price: number}} item - name non-empty, price > 0 * @param {number} quantity - positive integer ≥ 1 * @returns {Array} Updated cart items array * @throws {TypeError} If item or quantity types are wrong * @throws {RangeError} If price ≤ 0 or quantity < 1 * @invariant cart.total === sum of (price × qty) for all items */ - 5
Step 5 — Enforce with Guard Clauses and Assertions
Finally, we add runtime enforcement. Guard clauses at the top check preconditions, and an assertion at the bottom verifies the invariant:function addItem(item, quantity) { // Precondition guards if (typeof item?.name !== 'string' || item.name.length === 0) { throw new TypeError('item.name must be a non-empty string'); } if (typeof item?.price !== 'number' || item.price <= 0) { throw new RangeError('item.price must be > 0'); } if (!Number.isInteger(quantity) || quantity < 1) { throw new RangeError('quantity must be an integer ≥ 1'); } // Function body this.items.push({ ...item, qty: quantity }); this.total += item.price * quantity; // Invariant assertion const expectedTotal = this.items.reduce((s, i) => s + i.price * i.qty, 0); console.assert(Math.abs(this.total - expectedTotal) < 0.01, 'Invariant broken'); return this.items; }
SECTION 7
Strengths & Limitations of Contracts
Function contracts are powerful, but they come with tradeoffs. Adding validation code takes time to write and slightly slows down execution. On the other hand, skipping contracts can lead to hours of debugging mysterious failures. The table below compares the key strengths and limitations so you can make informed decisions about when to invest in thorough contracts.
| Strengths | Limitations |
|---|---|
| Bugs are caught early — at the boundary where data enters, not deep inside the code. | Guard clauses add lines of code, which can feel verbose for small utility functions. |
| Self-documenting: a contract tells other developers exactly how to use the function. | Runtime checks have a small performance cost, especially in hot loops called thousands of times. |
| Invariants prevent entire categories of state corruption. | Contracts cannot catch every possible bug — they only check what you explicitly test. |
| Easier to test: each precondition and postcondition becomes a natural test case. | Keeping contracts up-to-date requires discipline as requirements change. |
Tradeoffs of using function contracts in JavaScript
WHEN TO GO ALL-IN ON CONTRACTS
SECTION 8
Connection to Advanced Concepts
Function contracts are the first step on a ladder that leads to more sophisticated techniques in professional software engineering. As you advance, you will encounter tools that automate or extend what we have been doing by hand.
| What You Learned Today | Advanced Version | How It Extends the Idea |
|---|---|---|
| JSDoc contract comments | TypeScript interfaces & types | The compiler checks type contracts automatically at build time — no runtime cost. |
| Guard clauses with if/throw | Schema validation (Zod, Joi) | Declarative schemas replace hand-written guards, auto-generating error messages. |
| console.assert for invariants | Property-based testing (fast-check) | Randomly generates thousands of inputs to verify invariants hold in every scenario. |
| Informal @invariant tags | Formal verification | Mathematical proof that your code satisfies its contract for all possible inputs. |
How today's concepts connect to advanced tools
The core insight does not change as you move up this ladder: every function is a promise, and the clearer that promise is stated, the more reliable the software becomes. Whether you express the contract in a comment, a type annotation, a validation schema, or a formal proof, you are always answering the same two questions — "What must be true for this to work?" and "What do I promise in return?"
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
function divide(a, b). Write a JSDoc comment that includes a @param tag for each input, a @returns tag, a @throws tag, and state at least one precondition.Show answer
/** @param {number} a - the dividend, @param {number} b - the divisor, must not be 0, @returns {number} the quotient a/b, @throws {RangeError} if b is 0 */. The critical precondition is that b !== 0, because division by zero is undefined. Both parameters should also be of type number. The postcondition guarantees that the return value equals a / b.Question 3: INTERMEDIATE
function setGrade(student, score) where score should be between 0 and 100 inclusive, and student must have a name property. Write the guard clauses that enforce these preconditions, and then write an assertion that checks the invariant: no student's grade is ever set outside the 0–100 range.Show answer
if (typeof student?.name !== 'string') throw new TypeError('student must have a name'); if (typeof score !== 'number' || score < 0 || score > 100) throw new RangeError('score must be 0–100'); After assigning the grade (student.grade = score), you would add: console.assert(student.grade >= 0 && student.grade <= 100, 'Invariant violated: grade out of range'); This ensures the invariant is checked at runtime. The guard clauses prevent bad input from entering, while the assertion provides a safety net at the output.Question 4: APPLIED
removeSong(playlist, songId) function should remove a song from the playlist array. Write the full contract (preconditions, postconditions, invariant, and side effects) as a JSDoc comment. Then explain what could go wrong if you skip the invariant.Show answer
/** @param {Array} playlist - non-empty array of song objects, @param {string} songId - must match an existing song's id, @returns {Array} playlist without the specified song, @throws {RangeError} if songId not found, @sideeffect Mutates the playlist array, @invariant playlist contains no duplicate song IDs */. The postcondition is that the returned array's length is one less than the original, and the specified song is no longer present. If you skip the invariant, it is possible for a bug in another function (like addSong) to insert a duplicate ID. Then removeSong might only remove one copy, leaving a ghost duplicate that confuses the UI.Question 5: CRITICAL THINKING
Show answer
addItem's invariant assertion can reveal a bug in the pricing logic. Finally, writing contracts forces you to think clearly about edge cases before you code, which often prevents bugs from being written in the first place.SUMMARY
Lesson Summary
Summary
A function contract is a clear statement of the agreement between a function and its caller. It consists of three parts: preconditions that define valid inputs (what the caller must provide), postconditions that define guaranteed outputs (what the function promises to return), and invariants that define conditions which must remain true before and after every call. In JavaScript, contracts are expressed through JSDoc comments, enforced with guard clauses at the top of functions, and verified with assertions before returning.
Contracts belong to a tradition stretching from Hoare Logic (1969) through Design by Contract (1986) to modern tools like TypeScript and Zod. They are most valuable at system boundaries where external data enters your code. Mastering contracts trains you to think precisely about what each function needs and promises, making you a clearer communicator and a more reliable developer.