Using Racket Contracts to Guard Module Interfaces
Learn how Racket's contract system can enforce pre‑ and post‑conditions on module exports, illustrated with a bounded stack example and a discussion of runtime overhead.
26 Jan 2026, 21:00 UTC

The problem: silent interface bugs in growing codebases
As a Racket project expands, modules start to depend on each other’s exported functions. A change that accidentally violates an assumed invariant—like pushing onto a full stack or popping from an empty one—can propagate far before any test fails, making debugging painful.
We need a lightweight way to declare those assumptions directly alongside the code, so violations surface immediately with a clear message.
Thesis: contracts turn interface expectations into executable checks
Racket’s contract system lets you attach preconditions, postconditions, and invariants to any exported identifier. When a contract fails, Racket raises an exception that points to the exact module and the broken condition, turning a latent bug into an immediate, actionable error.
Worked example: a bounded stack with push/pop contracts
First, create a file stack.rkt that exports a stack structure and two operations, each guarded by a contract.
#lang racket
(struct stack (items max-size) #:transparent)
(define (make-stack max)
(stack empty max))
(define/contract (push s v)
(-> stack? any/c stack?)
; precondition: stack not full
(define len (length (stack-items s)))
(unless (< len (stack-max-size s))
(error 'push "stack is full"))
(stack (cons v (stack-items s)) (stack-max-size s)))
(define/contract (pop s)
(-> stack? (values any/c stack?))
; precondition: stack not empty
(define len (length (stack-items s)))
(when (zero? len)
(error 'pop "stack is empty"))
(define v (first (stack-items s)))
(values v (stack (rest (stack-items s)) (stack-max-size s))))
(provide/make-stack push pop)
The define/contract forms wrap each function with a contract expressed as a predicate. The arrow notation -> describes the domain and range; the body contains the actual precondition checks. If a caller violates a precondition, Racket raises an exception like:
push: contract violation
expected:
given: #
in: the push function defined at stack.rkt:12:0
contract violation
precondition failed
expected: (< length (stack-items s)) < (stack-max-size s)
given: 5
in: the push function defined at stack.rkt:12:0
Running the module with racket -t stack.rkt (the -t flag enables contracts) will surface the error as soon as the illegal call occurs.
Trade‑off: runtime overhead versus safety
Each contracted call incurs the cost of evaluating its predicates. In tight loops—say, pushing thousands of items—this overhead can become noticeable. The contract system lets you disable checks for production builds:
- Use
#:contract-out #fwhen compiling:raco make -j0 #:contract-out #f stack.rkt - Or keep contracts enabled only in development by wrapping the module in
(require (planet ...))blocks that are omitted in release.
A practical way to assess the impact is to time a representative workload with contracts on and off, using the built‑in time form or external /usr/bin/time. Remember to keep contract predicates pure and inexpensive; otherwise they can mask real bugs or introduce new ones.
When to enable contracts
Enable contracts:
- During development and testing to catch interface mistakes early.
- In continuous‑integration pipelines where a contract failure should break the build.
- At the boundaries of subsystems where correctness is critical (e.g., public APIs).
Disable or sample‑check contracts in performance‑sensitive production paths, relying on a solid test suite to have already exercised the contracts.
Actionable closing
Start by identifying a module whose invariants are easy to state—like a bounded stack, a queue with a maximum length, or a resource counter that must never go negative. Write a define/contract or provide/contract for each exported function, run your test suite with racket -t, and observe any contract violations. Measure the overhead on a realistic workload; if it’s acceptable, keep contracts enabled in your CI builds. If not, enable them only for debugging and keep them off in release builds.
By treating contracts as executable documentation, you turn implicit assumptions into explicit, checkable guarantees, making large Racket projects safer to evolve.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.