Catch Module Boundary Bugs with Racket Contracts
Use provide/contract to wrap Racket exports with runtime predicates and get precise blame errors when modules violate their agreements.
13 Aug 2025, 05:14 UTC

The problem: a bad argument reaches a module function
Imagine a module that exports a geometry function expecting two numbers. A caller passes a string instead. If the function uses a plain assert or lets Racket's arithmetic operator fail, the error appears inside the provider module. It does not say whether the caller or the provider broke the agreement, and it forces you to trace backward through the call stack to find the real culprit.
The answer: provide/contract wraps exports with predicates
racket/contract lets you attach runtime predicates to exported values. A contract such as (-> number? number? number?) promises that area receives two numbers and returns a number. Racket checks both sides of the module boundary: the caller's arguments and the provider's result. When a check fails, Racket raises a contract violation with blame, naming the responsible module and pointing to source locations.
Worked example: a geometry module and a bad caller
Create geometry.rkt:
#lang racket
(require racket/contract)
(provide/contract
[area (-> number? number? number?)])
(define (area width height)
(* width height))
Create bad-client.rkt in the same directory:
#lang racket
(require "geometry.rkt")
(area 10 "wide")
Run the example and read the blame report
From the directory containing both files, run:
racket bad-client.rkt
No special permissions are required; the command only reads and executes local files. Expect a contract violation exception. The message should name bad-client.rkt as the party that supplied the invalid argument, because the contract on area promises numbers and the client supplied a string. If geometry.rkt instead returned a string, the blame would point at the provider module.
Turn checking off temporarily to confirm the cause
To verify that the exception comes from the contract, wrap the call in the contract-checking parameter:
#lang racket
(require racket/contract)
(require "geometry.rkt")
(parameterize ([contract-checking #f])
(area 10 "wide"))
With checking disabled, the same bad input no longer raises a contract exception. It may produce a meaningless result or fail later with a lower-level error. Use this only as a diagnostic; disabling contracts in production removes the boundary guarantee.
Limits and common mistakes
- Contracts are runtime-only. They do not catch mismatches at compile time and add overhead on every boundary crossing. Avoid wrapping hot inner loops; apply contracts at module boundaries and public APIs.
- They are not static types. If you need compile-time guarantees, use
#lang typed/racketinstead. - Forgetting
(require racket/contract)or using plainprovideleaves exports unchecked. The code runs, but boundary violations surface as opaque errors deep inside the callee. - Higher-order blame can point at the module that built a bad function value, not the immediate caller that invoked it.
- Mutable struct contracts can observe temporarily broken invariants during an update and raise false violations.
Practical verification
Run the module in DrRacket or with racket, pass a string where a number is expected, and confirm a contract violation with blame. Then disable checking with parameterize and confirm the same bad input no longer raises a contract error. If you do not see a blame error, check that racket/contract is required and that the export uses provide/contract, not provide.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.