Using Racket Contracts to Catch Runtime Errors Early
Learn how Racket’s contract system lets you attach pre‑ and post‑conditions to functions, get blaming exceptions when they fail, and toggle checks for production builds.
11 Feb 2026, 19:09 UTC

Problem: Silent bugs in numeric code
Imagine a helper that computes a square root only for non‑negative numbers. If a caller accidentally passes a negative value, the function might return NaN or raise an obscure math error later in the call stack, making the root cause hard to trace.
Thesis: Contracts turn expectations into immediate, blame‑aware failures
Racket’s racket/contract library lets you wrap a function with executable specifications. When a contract is violated, the runtime raises a contract-blame exception that points to the module responsible, turning a latent bug into an obvious, actionable failure.
1. Contracts as first‑class values
Contracts are values you can combine with combinators like -> (for simple functions), ->i (for independent pre‑ and post‑conditions), and flat-contract (for plain predicates). They do not change the function’s core logic; they merely add checks around calls.
2. Writing a contract with ->
The -> combinator takes a domain contract, a range contract, and the function body. For a square‑root helper that expects a non‑negative real and returns a non‑negative real, the contract looks like this:
#lang racket
(require racket/contract)
(define (safe-sqrt x)
(sqrt x))
(define contracted-safe-sqrt
(contract-out
[safe-sqrt (-> real? real?)])))
The contract-out form exports safe-sqrt wrapped by the contract. The domain real? ensures the argument is a real number; the range real? ensures the result is also real (which sqrt guarantees for non‑negative inputs).
3. Worked example: detecting a bad call
Save the above as sqrt-demo.rkt and run it:
$ racket sqrt-demo.rkt
> (contracted-safe-sqrt -4)
Because -4 fails the domain contract (real? passes, but the implicit expectation of non‑negativity is not captured), we need a stronger domain. Let’s refine the contract using a flat predicate:
(define (non-negative? x) (and (real? x) (>= x 0)))
(define contracted-safe-sqrt
(contract-out
[safe-sqrt (-> non-negative? real?)])))
Now running the same call produces:
function contract violation:
expected: non-negative?
given: -4
in: the safe-sqrt function
contract from: (definition contracted-safe-sqrt)
blaming: sqrt-demo.rkt
(assuming the contract is correct)
at: eval:2.0
The exception tells us exactly which module (sqrt-demo.rkt) supplied the bad argument, making debugging straightforward even in larger projects.
4. Trade‑offs and limitations
- Runtime overhead: Each contract adds a function call and predicate evaluation. In tight loops or performance‑critical sections, this can be noticeable.
- Brittleness: Over‑specifying contracts (e.g., checking deep properties of large data structures) can make refactoring painful because every contract must be updated.
- Selective enabling: Contracts can be turned off per module with
#:lang racket/contractor globally via the environment variablePLT_CONTRACTS=disabled. This lets you keep contracts active in development and test suites while disabling them for production builds.
To verify that disabling works, compile the file and run it with the variable unset, then set it:
$ raco make sqrt-demo.rkt
$ racket sqrt-demo.rkt # contracts enabled → exception on bad input
$ PLT_CONTRACTS=disabled racket sqrt-demo.rkt # no exception, function returns NaN
You can also inspect the generated bytecode to see the extra calls:
$ raco disassemble sqrt-demo_zo
The output will show wrapper functions around the original safe-sqrt body.
Closing: Make contracts part of your workflow
Start by adding lightweight contracts to public functions that have clear pre‑ or post‑conditions (e.g., “input must be a non‑empty list”, “output must be sorted”). Run your test suite with contracts enabled to catch regressions early. When you prepare a release, build with PLT_CONTRACTS=disabled to strip the overhead. This approach gives you safety during development without sacrificing performance in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.