View Source Configuring Contracts
Contracts can be switched off at runtime or removed at compile time, per kind and per module. This guide covers how, and the rules that govern which combinations are meaningful.
For what contracts cost when they are on — measured, per call — see the Overhead guide.
The three modes
Bond reads four application-config keys at compile time. Each accepts one of three values:
| Value | Compiled? | Runtime behaviour | Doc section? |
|---|---|---|---|
true | yes | evaluated unless disabled via Bond.Config | yes |
false | yes | skipped unless enabled via Bond.Config | yes |
:purge | no | n/a — there is no code to run | no |
The keys are :preconditions, :postconditions, :invariants, and
:checks. Each defaults to true.
# config/prod.exs — purge contracts entirely from this build
config :bond,
preconditions: :purge,
postconditions: :purge,
invariants: :purge,
checks: :purgeThe contract-checking chain
:preconditions, :postconditions, and :invariants form a chain:
preconditions ≤ postconditions ≤ invariantsA :postconditions failure is only diagnostically meaningful if
:preconditions held first — without preconditions, an "incorrect"
output might really be the caller's fault, not the callee's. Same for
:invariants resting on both. Bond enforces this in two ways:
Compile time. If a lower kind is
:purged, every higher kind must also be:purge. Mixing them produces aCompileErrorat config- resolution time with an explanation. To skip a kind's evaluation without removing the code, usefalseinstead of:purge.Runtime. If a lower kind is
falseat runtime (Bond.Config.disable(:preconditions)), the higher kinds are also skipped — even if they're set totruethemselves. Bond emits a one-time-per-processLogger.warningthe first time this happens for a given (higher, lower) pair, so the diagnostic is visible.
The chain is inherited from Eiffel, where assertion monitoring is a single ordered scale
— no, require, ensure, invariant, … — in which each level implies the ones before
it, for exactly this reason: "it does not make sense to monitor postconditions unless you
also monitor preconditions, since a routine is required to ensure its postcondition only
if it was called with its precondition satisfied" (Object-Oriented Software
Construction, 2nd edition, §11.13).
:checks is independent of the chain, and this is a deliberate departure — Eiffel puts
check at the top of the same scale. A check/1 is an internal assertion about your own
computation, not a contract with a caller, so nothing about its meaning depends on
whether a precondition was verified first. Keeping it separate lets you strip contracts
from a build while leaving internal assertions in, or the reverse.
# Valid: progressively purge from the top.
config :bond, invariants: :purge
# Valid: keep everything compiled in, runtime-disable invariants by default.
config :bond, invariants: false
# Compile error: lower purged, higher present.
config :bond, preconditions: :purge # postconditions and invariants still trueRuntime toggling
When a kind is compiled with true or false, Bond emits a runtime
guard on every contract evaluation. The guard reads the per-kind runtime
state and evaluates the contract unless that state is exactly false, so
contracts can be flipped on and off without recompiling. Use
Bond.Config:
# In IEx or a remote console, against a running release:
Bond.Config.disable(:preconditions) # dormant
Bond.Config.enable(:preconditions) # active again
Bond.Config.all() # inspect the global stateThe state is held in a single :persistent_term entry, lazily seeded
from application env on first use — so config :bond, … in both
config.exs and config/runtime.exs is honoured. A kind with no global
setting falls back to its compile-time default (including any per-module
:overrides).
Application.put_env/3is not live. Setting:bondapp env after the first contracted call has run has no effect — the runtime state is cached. UseBond.Configto toggle, orBond.Config.reset/0to re-seed from current application env.
:purge is the only value with no runtime presence — the code isn't
compiled in, so nothing can bring it back.
The runtime check is a single lock-free :persistent_term read per call
per contract kind — the whole reason false is affordable enough to leave
compiled into a production build. The Overhead guide has the
measured per-call figures for each kind in each mode.
The enabled (true) cost is dominated by the gate and by evaluating the
assertion expressions themselves — not by the function's size. The
failure binding() snapshot (reported in error messages) is captured
lazily and only materialised when an assertion actually fails, so the
per-call overhead does not grow with the number of parameters or
old(...) captures. A wide signature with old(...) postconditions pays
about the same as a one-argument one.
For genuinely hot-path code, prefer :purge.
Choosing what runs in production
Dev and test almost always want everything on. Production is a real decision, and the three modes combine into three postures worth knowing:
Everything purged. No contract code in the build at all.
# config/prod.exs
config :bond,
preconditions: :purge,
postconditions: :purge,
invariants: :purge,
checks: :purgePreconditions only. The chain requires that a purged kind has every kind above it purged — so the lowest kind is the one you can keep:
# config/prod.exs
config :bond,
preconditions: true,
postconditions: :purge,
invariants: :purge,
checks: :purgePreconditions have the best cost-to-value ratio of the three. They are the cheapest to
evaluate — roughly a third of an invariant, which fires twice per call and shape-tests the
return value (see Overhead) — and they are the only kind that catches a
caller's bug. That is exactly the failure you cannot reconstruct from the callee's
logs afterwards: a precondition violation names the caller and hands you the binding it
called with. This is close to Eiffel's own default posture, where monitoring is set to
require across the system and raised only for selected classes.
Compiled in, switched off. Use false instead of :purge to keep the checks in the
build but inert, so they can be enabled from a remote console mid-incident:
# config/prod.exs
config :bond, preconditions: false, postconditions: false, invariants: false# then, in a remote console on the running release
Bond.Config.enable(:preconditions)You pay the runtime gate on every call (a single :persistent_term read) and nothing for
the assertions until you switch them on. Mixing postures is fine, and
per-module overrides are how you do it — purge the hot paths,
keep preconditions everywhere else.
Per-module overrides
Use :overrides in your :bond config to make exceptions to the global
defaults. Each entry is {Module | Regex, opts}. Module-atom keys match
exactly; Regex keys match against the source-visible module name (no
Elixir. prefix).
config :bond,
preconditions: true,
postconditions: true,
overrides: [
{MyApp.HotPath, preconditions: :purge, postconditions: :purge, invariants: :purge},
{~r/Workers\./, postconditions: false}
]Precedence (most specific wins):
use Bond, optson the using module (highest).:overridesentry whose key is an exact module atom.:overridesentry whose key is a regex (first match in list order wins).- Global
:bondconfig (lowest).
A module can also opt out (or in) directly at the use site:
defmodule MyApp.HotPath do
use Bond, preconditions: :purge, postconditions: :purge, invariants: :purge
endThe same top-down rule applies here as in global config: a :purged kind
requires every kind above it to be :purged too, whether the setting arrives
from config :bond, an :overrides entry, or use Bond.