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.

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.

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.

Visual Explanation — Anatomy of a Function Contract

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.

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
/** * @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
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
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.

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.

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.
Common contract types with JavaScript examples
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

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 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
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
Step 3 — State the InvariantNo 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
Step 4 — Write the JSDoc CommentNow 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 AssertionsFinally, 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; }

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.

Tradeoffs of using function contracts in JavaScript
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.
💡 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.

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.

How today's concepts connect to advanced tools
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.

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?"

Practice Problems

PROBLEM 1 — CONCEPTUAL
In your own words, explain the difference between a precondition and a postcondition. Then explain why an invariant is different from both.
PROBLEM 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.
PROBLEM 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.
PROBLEM 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.
PROBLEM 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.

Lesson 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