Bond

Design By Contract for Elixir.

Bond lets you attach preconditions and postconditions to your functions and verify them at runtime. A contract is a plain Elixir boolean expression with optional labels:

defmodule Account do
use Bond
@pre positive_amount: amount > 0
@post non_negative_balance: result >= 0
def withdraw(balance, amount), do: balance - amount
end

When a contract fails, Bond raises a Bond.PreconditionError or Bond.PostconditionError with the failing assertion's label, expression, location, and the local binding — telling you exactly what went wrong and where.

Bond is an implementation of the Design By Contract methodology (also called programming by contract), introduced by Bertrand Meyer with the Eiffel language. See the About guide for background.

Usage

use Bond in any module to enable the @pre, @post, and @invariant annotations plus the check/1 macro. Contracts may use any Elixir expression that returns a boolean (or a truthy value).

defmodule Math do
use Bond
@pre numeric_x: is_number(x), non_negative_x: x >= 0
@post float_result: is_float(result),
non_negative_result: result >= 0.0,
"sqrt of 0 is 0": (x == 0) ~> (result === 0.0),
"sqrt of 1 is 1": (x == 1) ~> (result === 1.0),
"x > 1 implies result smaller than x": (x > 1) ~> (result < x)
def sqrt(x), do: :math.sqrt(x)
end

@pre and @post accept one or more labelled assertions. Preconditions have access to the function's parameters; postconditions also have access to the result variable (bound to the function's return value) and old(...) expressions that snapshot a value before the function runs (see old expressions below).

use Bond {: .info}

use Bond overrides Kernel.@/1 so that @pre, @post, @invariant, and @doc annotations can be intercepted and recorded, and installs @on_definition, @before_compile, and @after_compile compiler hooks that wrap functions with contracts via defoverridable at the end of module compilation. Your defs and defps are otherwise left alone.

use Bond also imports the Bond module so the check/1 macro is available, and imports Bond.Predicates so the predicate functions and operators defined there (such as ~> and |||) can be used in assertions. Bond.Predicates can be explicitly imported elsewhere if you want the operators outside of contract expressions.

Assertion syntax

An assertion is a boolean (or truthy) Elixir expression, optionally paired with a label. Labels are atoms or strings; they appear in error messages and generated documentation.

The recommended form is the keyword list, even for a single assertion:

@pre positive_x: x > 0
@post non_decreasing: result >= old(result)
@pre numeric_x: is_number(x), non_negative_x: x >= 0

For a bare assertion where a label adds no information, the bare form is also fine:

@pre is_number(x)
@post is_float(result)

The same two forms work for @invariant declarations and inside function bodies via the check/1 macro:

@invariant subject.capacity >= 0
@invariant non_negative_capacity: subject.capacity >= 0,
size_within_capacity: length(subject.items) <= subject.capacity
check is_number(x)
check x_is_number: is_number(x)

Bond also provides the Bond.Predicates module with operators that are often useful in assertions — notably ~> (logical implication) and <~ (pattern match). Bond.Predicates is automatically imported into assertion expressions, so you can use these operators directly:

@post (x == 0) ~> (result == 0.0)
@post {:ok, _} <~ result

See Bond.Predicates for the full list.

@invariant for struct modules

@invariant declarations specify properties that hold for every value of a struct, checked automatically on the way into and out of every public function in the struct's defining module.

Where @pre/@post constrain a single function call, @invariant constrains the struct itself — every instance produced by the module's public API satisfies the invariant, every instance entering its public API is expected to.

defmodule BoundedStack do
use Bond
defstruct [:items, :capacity]
@invariant non_negative_capacity: subject.capacity >= 0,
size_within_capacity: length(subject.items) <= subject.capacity
def new(capacity) when is_integer(capacity) and capacity >= 0 do
%__MODULE__{items: [], capacity: capacity}
end
def push(%__MODULE__{} = stack, item) do
%{stack | items: [item | stack.items]}
end
end

The subject binding

Inside an @invariant expression, subject refers to the struct instance being checked. Bond rebinds subject at every check site to whichever struct parameter the function head exposes — you write the invariant once against subject and Bond handles the rest, regardless of what each function names its struct parameter.

When invariants fire

Invariants check at the boundaries of public functions in the struct's module — the places a struct value crosses between "internal" (possibly transient) and "external" (must be valid). Bond auto-detects the struct parameter in any of these head shapes:

Function head shapeDetected?Pre-check on entry
def foo(%__MODULE__{} = name, ...)yesyes, on the captured struct
def foo(x, ...) when is_struct(x, __MODULE__)yesyes, on x
def foo(%__MODULE__{field: ...}, ...) (destructure-only)yesyes, on the captured struct
def foo(x, ...) (no pattern, no guard)noskipped silently
defp ... (any shape)noskipped — private functions exempt by Eiffel convention

The post-check on exit matches both %__MODULE__{} and {:ok, %__MODULE__{}} return shapes. Other shapes ({:error, _}, bare integers, etc.) fall through with no check. If your function returns the struct under a different wrapper, add an explicit @post.

Multiple struct parameters in the same head (e.g. def merge(%__MODULE__{} = a, %__MODULE__{} = b)) are all checked in left-to-right order; subject rebinds to each in turn.

Violation behaviour

A violated invariant raises Bond.InvariantError with the same metadata shape as Bond.PreconditionError / Bond.PostconditionError, and fires the same telemetry event ([:bond, :assertion, :failure] with :kind => :invariant). Test with Bond.Test.assert_invariant_violation/2.

What's not supported

Invariants are scoped to the struct's own defining module. External modules that operate on the struct can't declare invariants for it — this matches Eiffel's class-locality and keeps cross-module ownership clean.

Process-level invariants (for GenServer/Agent state) aren't a separate feature. The recommended pattern is to keep the process state in a struct and declare invariants on that struct's module. See the Contracts in a Concurrent World guide.

Inline check/1 assertions

Bond's check/1 macro places assertions at arbitrary points inside a function body — useful for sanity checks during development. It honours the :bond, :checks config (see Conditional compilation) and is safe to disable in production builds.

def total(items) do
raw = Enum.sum(items)
check raw >= 0
check total_is_integer: is_integer(raw)
raw
end

On success check returns the assertion's value (or list of values for the keyword-list form). On failure it raises Bond.CheckError.

When to use check {: .warning}

Don't use check for input validation, validating data from external systems, or anything else that protects the integrity of your code. If the check were removed (or compiled out via config), the system must still behave correctly. Use ordinary control flow for that.

old expressions

old expressions in postconditions snapshot a value before the function body runs, so the postcondition can compare the after-state to the before-state.

defmodule Counter do
use Bond
def get_count(agent), do: Agent.get(agent, & &1)
@post incremented: get_count(agent) == old(get_count(agent)) + 1
def increment_count(agent) do
Agent.update(agent, &(&1 + 1))
end
end

Bond resolves every old(...) expression at the start of function execution and threads the captured value into the postcondition. old is only available inside @post.

The naive form above has a race condition when used against stateful concurrent components — another increment_count/1 can interleave between the old snapshot and the postcondition evaluation. See the Contracts in a Concurrent World guide for the pattern that handles this. For struct-based state machines, @invariant is usually a better fit than old — it constrains every operation's input and output struct rather than a single delta.

Documenting contracts

Contracts are part of a module's public interface, in the same way that function signatures and typespecs are. Bond treats them that way: every function with a contract gets a #### Preconditions and/or #### Postconditions section appended to its @doc, formatted as the original assertion source. The sections appear in ex_doc output and in editors that show function docs on hover (VS Code, Vim's K, etc.).

Auto-generated contract sections appear whether or not you wrote a @doc yourself — Bond synthesises one when needed.

Conditional compilation and docs {: .info}

When a function has all of its contracts :purged (see Conditional compilation), the function runs with zero contract overhead and its auto-generated contract sections are also suppressed. If you want the contract documentation visible in production builds, leave at least one of :preconditions or :postconditions set to true or false (both emit the override; only :purge removes it).

Conditional compilation

Bond reads four application-config keys at compile time. Each accepts one of three values:

ValueCompiled?Runtime behaviourDoc section?
trueyesevaluated unless Application.put_env/3 flips ityes
falseyesskipped unless Application.put_env/3 flips ityes
:purgenon/a — there is no code to runno

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: :purge

The contract-checking chain

:preconditions, :postconditions, and :invariants form a chain:

preconditionspostconditionsinvariants

A :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:

:checks is independent of the chain. A check/1 is an internal assertion about your computation, not a contract with a caller, so it remains meaningful regardless of any other kind's settings.

# 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 :true

Runtime toggling

When a kind is compiled with true or false, Bond emits a runtime guard on every contract evaluation that reads Application.get_env(:bond, <kind>, <compile_time_value>). The guard evaluates the contract unless the runtime value is exactly false. This means contracts can be flipped on and off without recompiling:

# In IEx or a remote console, against a running release:
Application.put_env(:bond, :preconditions, false) # dormant
Application.put_env(:bond, :preconditions, true) # active again

:purge is the only value with no runtime presence — the code isn't compiled in, so Application.put_env/3 can't bring it back.

The runtime check is a single Application.get_env/3 lookup per call per contract kind. A trivial benchmark (a function with @pre is_number(x) called in a tight loop) shows:

Modens / callOverhead vs :purge
:purge~48 ns
false~89 ns~40 ns (the guard alone)
true~155 ns~107 ns (guard + assertion eval)

For genuinely hot-path code, prefer :purge. The benchmark itself lives at bench/runtime_check_overhead.exs if you want to reproduce it on your hardware.

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},
{~r/Workers\./, postconditions: false}
]

Precedence (most specific wins):

  1. use Bond, opts on the using module (highest).
  2. :overrides entry whose key is an exact module atom.
  3. :overrides entry whose key is a regex (first match in list order wins).
  4. Global :bond config (lowest).

A module can also opt out (or in) directly at the use site:

defmodule MyApp.HotPath do
use Bond, preconditions: :purge, postconditions: :purge
end

Telemetry

Bond emits a :telemetry event whenever a @pre, @post, @invariant, or check assertion is violated. The event fires once per failure, immediately before the corresponding Bond.PreconditionError / Bond.PostconditionError / Bond.InvariantError / Bond.CheckError is raised.

Event:[:bond, :assertion, :failure]

Measurements:

Metadata:

Attach a handler at application start:

:telemetry.attach(
"bond-failure-logger",
[:bond, :assertion, :failure],
&MyApp.Telemetry.log_bond_failure/4,
nil
)
defmodule MyApp.Telemetry do
require Logger
def log_bond_failure(_event, _measurements, metadata, _config) do
Logger.warning(
"bond #{metadata.kind} violated in " <>
"#{inspect(metadata.module)}.#{elem(metadata.function, 0)}/" <>
"#{elem(metadata.function, 1)}: #{metadata.expression}"
)
end
end

Only failure events are emitted. Pass events would be far too chatty for production use; if there's demand for them they can be added later behind an opt-in.

Property-based testing

Bond contracts compose naturally with StreamData property-based testing. The usual hard parts of PBT are generating inputs and writing an oracle that distinguishes right answers from wrong ones; Bond's contracts already supply the oracle at every call site. PBT then just feeds random inputs through already-instrumented code.

Bond.PropertyTest.contract_holds/2 ships in two forms.

Single function

defmodule MathTest do
use ExUnit.Case
use Bond.PropertyTest
contract_holds &Math.sqrt/1, args: [StreamData.float(min: 0.0)]
end

Generates a property block that calls Math.sqrt/1 with random non-negative floats. Any precondition, postcondition, or check violation fails the property; StreamData shrinks to the minimal failing input.

Module sequence (invariant-driven)

defmodule BoundedStackTest do
use ExUnit.Case
use Bond.PropertyTest
contract_holds BoundedStack,
constructors: [{:new, [StreamData.integer(1..100)]}],
transformers: [{:push, [StreamData.term()]}, {:pop, []}],
observers: [{:size, []}, {:peek, []}]
end

Generates random sequences of operations over a struct module. The constructor produces an initial struct; transformers thread state forward (they take the current struct as their first argument); observers take the struct but don't advance state. The module's @invariants fire on every operation entry and exit, so any violation in any operation shrinks back to the minimal failing sequence.

Form 2 supports %Mod{} and {:ok, %Mod{}} return shapes from constructors and transformers. {:error, _} terminates the sequence cleanly (an operation refusing isn't a contract violation). Other return shapes raise an ArgumentError — wrap your function or test it with Form 1.

Setup

stream_data is an optional dep of bond. Add it to your own deps to enable PBT:

def deps do
[
{:bond, "~> 0.16.1"},
{:stream_data, "~> 0.6", only: [:dev, :test]}
]
end

use Bond.PropertyTest raises a CompileError with an explanation if stream_data isn't on the path.

Installation

bond can be installed by adding it to your list of dependencies in mix.exs:

def deps do
[
{:bond, "~> 0.16.1"}
]
end

Documentation

Documentation is generated with ExDoc and published on HexDocs and be found at https://hexdocs.pm/bond/Bond.html.