jonnifyelixir
F5 / F5.04 / assertions

F5.04 · part 2 of 3

Assertions in Elixir

Elixir has no require or ensure keyword, so a contract is written in the language’s ordinary idioms — and each idiom fits a different term. Guards and pattern matching in a function head express preconditions on the shape of arguments. A with chain composes several checks and short-circuits on the first failure. Tagged tuples carry expected failures back to the caller. And raise — or a match that fails — crashes on a broken invariant, which is a bug rather than a bad request. Choosing the right one is choosing how a violation should be handled.

Four idioms, four kinds of check. Select one to see which part of a contract it expresses.

Four idioms

The split that matters is between expected failures and impossible states. A not-found or an already-enrolled is expected: it flows back as {:error, reason} and the caller decides what to do. A negative progress value is impossible if the invariant holds, so reaching it is a defect — it should crash, not be handled. Select an idiom.

The contract toolkit · select an idiom

guards shape precondition with compose checks tagged tuple expected failure raise broken invariant expected failures return a tuple · impossible states crash

idiom: with

expresses: composed preconditions

There is no contract keyword because there does not need to be. Guards, with, tuples, and raise already say everything — you only have to pick the one that fits the failure.

In code

The whole contract, in one function. The head’s binary patterns are the shape precondition; the with composes the runtime checks and returns tagged tuples for the expected failures; and the match in build/2 asserts the invariant, crashing if it is ever false.

# guard via pattern: a malformed id never matches — the shape precondition
def enroll(<<"USR", _::binary>> = user_id, <<"CRS", _::binary>> = course_id) do
  with :ok <- ensure_not_enrolled(user_id, course_id),   # expected failure -> {:error, _}
       enrollment = build(user_id, course_id),
       :ok <- Portal.Store.put(enrollment) do
    {:ok, enrollment}
  end
end

def enroll(_, _), do: {:error, :bad_reference}   # precondition unmet, the caller's fault

defp build(u, c) do
  e = %Enrollment{id: Portal.ID.new("ENR"), user_id: u, course_id: c}
  true = e.progress in 0..100    # invariant assertion — raises MatchError if ever false
  e
end

match the idiom to the failure

Shape → guard; expected → tuple; composition → with; impossible → raise.

one honest command

Precondition, expected errors, and invariant, all visible in enroll.

Next: failing fast — why every one of these checks belongs at the boundary, before any state changes.

References

Primary sources for this lesson, and where it connects in the course.

Sources

Related in this course