Racket Contracts: Turning Module Assumptions into Checked Boundaries
Racket contracts turn informal module assumptions into checked boundaries. See how `provide/contract` and `->i` catch API misuse early, and what they cost at runtime.
29 Jan 2026, 00:37 UTC

The Boundary Problem
When two modules meet, assumptions travel in silence. A function expects a sorted list; the caller sends whatever is convenient. Nothing complains until a downstream computation produces nonsense, and by then the original mistake is buried under stack frames. In Racket, the contract system exists to break that silence at the exact point where data crosses a module boundary.
A contract is an executable claim about a value. It can be a simple predicate such as integer?, or a richer obligation that relates a function's inputs to its outputs. When you attach a contract to an export, you turn an informal comment into a checked boundary that fails fast.
Wrapping Exports with provide/contract
The most common entry point is provide/contract. Instead of a plain provide, you list each exported name alongside the contract it must satisfy. The contract is checked whenever another module imports and calls the function.
Suppose a utility module exports a function that doubles a number. A basic contract looks like this:
(provide/contract
(-> integer? integer?))
If a caller passes a string, Racket raises a contract violation immediately. The error message assigns blame to the module that supplied the bad argument, not to the utility module that detected it. That distinction matters: blame points to the guilty party, which is usually the caller, but beginners often expect the error to surface inside the callee.
Dependent Contracts with ->i
Simple predicates cannot express invariants that depend on multiple values. Racket's ->i form fills that gap. It lets you name the arguments and the result, then write a postcondition that references them.
Consider a module that filters and sorts test scores. The function should accept a list of integers and a minimum threshold, and it must return a sorted list containing only scores at or above the threshold. A dependent contract can enforce both properties:
#lang racket
(define (sorted-ascending? lst)
(or (null? lst)
(null? (cdr lst))
(and (< (car lst) (cadr lst))
(sorted-ascending? (cdr lst)))))
(provide/contract
(->i ([raw-scores (listof integer?)]
[min-score integer?])
[result (listof integer?)]
#:post (result raw-scores min-score)
(and (sorted-ascending? result)
(andmap (lambda (s) (>= s min-score)) result))))
(define (top-scores raw-scores min-score)
(sort (filter (lambda (s) (>= s min-score)) raw-scores) <))
Save this as scores.rkt and run it with racket scores.rkt or load it in DrRacket. If you call top-scores from another module with a list that is not all integers, the contract on raw-scores fires before the function body executes. If the function ever returned an unsorted list—say, because a future edit removed the sort call—the #:post clause would catch it at the boundary.
The trade-off is readability. A postcondition that walks a list on every call is more expensive than integer?, and it can obscure the function's intent for readers who are not fluent in the contract syntax. Because contract syntax has evolved across Racket versions, confirm the exact forms against the Guide and Reference documentation for your installed release.
The Runtime Cost and a Pragmatic Strategy
Contracts are dynamically checked by default. Every call to a contracted function evaluates the relevant predicates and postconditions. For tight loops or high-throughput servers, that overhead is real. Racket's time form gives you a quick way to measure it on a representative workload:
(time (top-scores big-list 50))
Run this once with the contract in place and once with a plain provide to see the difference. Because the cost scales with contract complexity, a simple predicate may be negligible while a dependent postcondition that sorts or scans a large list will add measurable latency.
A common engineering decision is to keep contracts active during development and testing, where they document assumptions and catch integration mistakes, then selectively relax or remove them on performance-critical production paths. This is not an all-or-nothing choice: you can contract the public API of a subsystem while leaving internal, already-trusted calls unchecked.
Where Contracts Fit
Contracts are not a static type system. They cannot prove that your program is free of errors before it runs. What they do exceptionally well is guard the seams—the places where third-party input, configuration data, or another team's module first enters your code. If you treat those seams as the right place to spend checking effort, contracts become a practical tool rather than a tax.
Start by auditing your module exports. Pick the two or three functions most likely to receive bad data, wrap them with provide/contract, and add an ->i postcondition only where a cross-argument invariant truly matters. Then benchmark. If the overhead is acceptable, leave them in place; if not, relax the expensive checks and rely on tests for the rest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.