JavaScript • Software Craft and Communication

Function Contracts & Invariants — State function contracts (inputs/outputs) and invariants

Learn to define clear promises about what your functions expect, return, and guarantee every single time they run.

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 8 figures

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

Computer scientist Tony Hoare published a formal system for reasoning about programs using preconditions and postconditions — the intellectual ancestor of function contracts.

1986 • Design by Contract

Bertrand Meyer coined the term Design by Contract while creating the Eiffel programming language, embedding contracts directly into the language syntax.

1995 • JavaScript Is Born

Brendan Eich created JavaScript in ten days. The language had no built-in type system, making informal contracts and clear documentation even more important for writing reliable code.

2012 • TypeScript & JSDoc Contracts

Microsoft released TypeScript, giving JavaScript developers a way to express input/output contracts through static types. Meanwhile, JSDoc comments became a popular way to document contracts in plain JavaScript.

2020s • Runtime Validation Libraries

Libraries like Zod and io-ts brought runtime contract enforcement to JavaScript, letting developers validate data shapes at the boundaries of their programs.

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.

1

Precondition (Inputs)

A precondition defines what must be true about the arguments before the function is called. For example, a divide function requires that the divisor is not zero.
2

Postcondition (Outputs)

A postcondition defines what the function guarantees about its return value and any side effects, assuming the precondition was met.
3

Invariant

An invariant is a condition that stays true across every call. For example, a bank-account balance should never go negative, no matter which functions run.
4

Side Effects

A complete contract also lists side effects — any changes the function makes outside its own scope, such as writing to a database or modifying a global variable.
✦

KEY TAKEAWAY

Think of a function contract like a vending machine label. The label says: "Insert a dollar bill (precondition), and you will receive one bag of chips (postcondition)." An invariant is the rule that the machine's total inventory count can never be negative — no matter how many transactions happen.

SECTION 3

Visual Explanation — Anatomy of a Function Contract

Anatomy of a Function ContractPRECONDITIONSamount: numberamount > 0amount ≤ balanceFUNCTION BODYwithdraw(amount)balance -= amountreturn newBalancePOSTCONDITIONSreturns: numberresult ≥ 0result = old − amountINVARIANTbalance ≥ 0 (always true before & after)No money is created or destroyedSIDE EFFECTS (documented)Logs transaction to console · Mutates internal balance state
This diagram shows a withdraw function's full contract. The cyan box on the left lists preconditions (what the caller must provide). The pink box on the right lists postconditions (what the function guarantees). The green box below shows the invariant — a truth that holds before and after every call. The dashed amber box notes any side effects.

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

/∗∗∗@paramnumberamount−Mustbe>0and≤balance∗@returnsnumberThenewbalance(≥0)∗@throwsRangeErrorIfamountisoutofrange∗@invariantbalance≥0∗//** * @param {number} amount - Must be > 0 and ≤ balance * @returns {number} The new balance (≥ 0) * @throws {RangeError} If amount is out of range * @invariant balance ≥ 0 */

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

functionwithdraw(amount)if(typeofamount!==′number′∣∣amount<=0)thrownewTypeError(′amountmustbeapositivenumber′);if(amount>this.balance)thrownewRangeError(′amountexceedsbalance′);//...functionbody...function withdraw(amount) { if (typeof amount !== 'number' || amount <= 0) { throw new TypeError('amount must be a positive number'); } if (amount > this.balance) { throw new RangeError('amount exceeds balance'); } // ... function body ... }

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

constnewBalance=this.balance−amount;console.assert(newBalance>=0,′Invariantviolated:balance<0′);returnnewBalance;const newBalance = this.balance - amount; console.assert(newBalance >= 0, 'Invariant violated: balance < 0'); return newBalance;

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.

Taxonomy of Function ContractsFUNCTION CONTRACTTYPE CONTRACTSEMANTIC CONTRACTPrimitive Typesstring, numberboolean, nullShape Types{name: string, age: number}Range / Valueage ≥ 0price > 0Relationshipstart < endsum = a + bINVARIANTSLoop InvariantTrue every iterationData InvariantTrue for data structureClass InvariantTrue for entire objectInvariants apply across functions, not just within one.
Contracts split into type contracts (what kind of data?) and semantic contracts (what values make sense?). Invariants are shown at the bottom because they span across multiple functions rather than belonging to just one.
Contract TypeExample in JavaScriptWhat It Prevents
Type (primitive)typeof x === 'number'Passing a string where a number is expected
Type (shape)'name' in user && 'age' in userMissing required properties on an object
Semantic (range)age >= 0 && age <= 150Nonsensical values like a negative age
Semantic (relationship)startDate < endDateAn event ending before it starts
Invariantitems.length === uniqueCountDuplicate 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. 1

    Step 1 — Identify the Inputs (Preconditions)

    The function takes two arguments: item (an object with name and price properties) and quantity (a positive integer). The preconditions are: item.name must be a non-empty string, item.price must be a number greater than zero, and quantity must be a whole number ≥ 1.
    Preconditions: typeof item.name === 'string' && item.name.length > 0 && item.price > 0 && Number.isInteger(quantity) && quantity >= 1
  2. 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 exactly item.price × quantity.
    Postcondition: newTotal === oldTotal + item.price × quantity
  3. 3

    Step 3 — State the Invariant

    No matter how many items are added or removed, the cart's total property must always equal the sum of price × quantity for 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. 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. 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.

StrengthsLimitations
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

Invest the most effort in contracts at the boundaries of your system — where data arrives from the outside world (user input, API responses, database queries). Internal helper functions that are only called by your own code can often rely on lighter documentation. Think of it like airport security: you check everybody at the entrance, not at every gate inside the terminal.

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 TodayAdvanced VersionHow It Extends the Idea
JSDoc contract commentsTypeScript interfaces & typesThe compiler checks type contracts automatically at build time — no runtime cost.
Guard clauses with if/throwSchema validation (Zod, Joi)Declarative schemas replace hand-written guards, auto-generating error messages.
console.assert for invariantsProperty-based testing (fast-check)Randomly generates thousands of inputs to verify invariants hold in every scenario.
Informal @invariant tagsFormal verificationMathematical 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

In your own words, explain the difference between a precondition and a postcondition. Then explain why an invariant is different from both.
Show answer
A precondition is a rule about what the caller must provide before the function runs — it describes valid inputs. A postcondition is a rule about what the function promises to deliver after it finishes — it describes valid outputs and effects. An invariant is different because it is not tied to one call; it is a condition that must remain true across every function call throughout the lifetime of the data. For example, 'the array must be sorted' is an invariant if every function that touches the array must preserve that sorted order.

Question 2: BASIC CALCULATION

Consider this function signature: 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
A correct JSDoc would be: /** @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

You have a function 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
The guard clauses would be: 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

You are building a playlist app. The 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
A good contract would be: /** @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

A classmate argues: "Guard clauses are a waste of time because if I'm the only one calling the function, I already know what inputs to pass." Write a response that addresses at least three specific reasons why this reasoning is flawed. Use examples from the lesson to support your argument.
Show answer
First, even solo developers forget their own assumptions after a few weeks — the JSDoc contract serves as a note to your future self. Second, code rarely stays private: teammates, open-source contributors, or even automated tests may call your function, and without contracts they have no guidance. Third, guard clauses protect against data from external sources (APIs, user input) that you do not control; even if your own calls are correct, a corrupted API response can violate preconditions silently. Fourth, invariant checks catch bugs that originate in entirely different functions — the shopping cart example showed that 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.

Varsity Tutors • JavaScript • Function Contracts & Invariants