Racket Contracts: Let the Runtime Tell You Which Module Broke the Deal
Racket's contract system checks module boundaries at runtime and, crucially, assigns blame to the module that actually broke the agreement — turning vague downstream crashes into one-line diagnoses.
16 Nov 2025, 13:55 UTC

Every multi-module program has the same quiet failure mode: module A passes a string where module B expected an integer, the error surfaces three calls later inside B, and the stack trace blames the wrong team. Racket's contract system attacks exactly this. You attach a machine-checkable agreement to an export, and when the agreement is violated, the runtime names the guilty party — not the code where the crash happened.
The thesis: contracts are not just type checks you write by hand. They are first-class values with a blame mechanism that understands module boundaries, and that blame is the feature worth adopting even if you never use the fancy combinators.
What a contract actually is
In Racket, a contract is a value that wraps another value and checks it at the moment it crosses a module boundary. The most common way to attach one is provide/contract (or the newer contract-out form):
#lang racket
(module server racket
(provide (contract-out
[deposit (-> integer? integer?)]))
(define (deposit amount)
(+ 100 amount)))
(module client racket
(require (submod ".." server))
(deposit "fifty"))Run the client module (in DrRacket or with raco run, no special permissions needed) and the contract system reports that client broke the contract on deposit, blaming the caller — not the deposit function itself. Swap the bug so the server returns a string, and the blame flips to the server. That flip is the whole point: the system tracks positive blame (the exporting module broke its own promise) and negative blame (the importer misused the export).
Why blame beats a plain predicate
You could write (unless (integer? x) (error ...)) at the top of every function. That tells you a check failed, but not whose fault it is, and it scatters validation logic through your implementation. Contracts separate the specification from the code, live at the boundary where the misunderstanding actually occurs, and produce error messages that name the module, the contract, and the offending value. In a codebase with a dozen modules, that turns a debugging session into reading one line of output.
A worked example with a dependent contract
Contracts compose. Beyond simple -> function contracts, you can express postconditions that refer to the inputs — something most type systems cannot do:
(provide
(contract-out
[withdraw (->i ([balance integer?]
[amount (balance) (and/c integer?
(lambda (a) (<= a balance)))])
[result (balance amount)
(lambda (r) (= r (- balance amount)))])]))
(define (withdraw balance amount)
(- balance amount))Here ->i is the dependent contract form: the contract on amount can mention balance, and the postcondition on result can mention both. This encodes "you can't withdraw more than you have" and "the result is exactly the difference" as runtime-checked boundary guarantees. A caller passing (withdraw 100 150) gets blamed immediately, with the violated clause spelled out.
Interaction with Typed Racket and testing
If you migrate a module to Typed Racket, the type annotations automatically become contracts at the boundary with untyped code — you get gradual typing without writing the contracts twice. Separately, raco contract random <module> generates random inputs that exercise your exported contracts, which is a cheap way to property-test the boundary: write a deliberately buggy implementation and confirm the tool finds a violation before trusting it on real code.
Trade-offs you should know before adopting
- Cost lives at the boundary. Checks run when values cross modules, so a hot function called millions of times across a contract boundary can measurably slow down. Use
(require racket/contract/profiling)and(contract-profiling-on)to measure before and after; within a fully typed module the compiler can eliminate redundant checks. - Keep contract predicates pure. A predicate with side effects can run multiple times (higher-order contracts wrap functions and check on each call), producing confusing blame or duplicated effects.
- Contracts don't check termination. A diverging predicate hangs your program at the boundary.
- Mutable data is only checked at crossing points. Internal mutation inside a module is unmonitored unless you opt into chaperone contracts.
Where to start
Pick one module boundary where bugs keep appearing — usually the one between two people's code — and add contract-out to its exports. Run the existing test suite; if blame messages appear, you just found a real disagreement. Then profile one realistic workload to confirm the overhead is acceptable. That single boundary is enough to judge whether contracts earn a permanent place in your project.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.