Elixir basics · Lesson 18 (open contents)
Make success and failure predictable
Use tagged tuples, `with`, and a small error contract.
Run this first
Run it once, change one input, and compare the new result.
# Keep every outcome inside one result contract
parse_positive = fn text ->
with {number, ""} <- Integer.parse(text), true <- number > 0 do
{:ok, number}
else
:error -> {:error, :not_an_integer}
{_number, rest} -> {:error, {:trailing_text, rest}}
false -> {:error, :not_positive}
end
end
{parse_positive.("7"), parse_positive.("7x"), parse_positive.("0")}- The results are
{:ok, 7},{:error, {:trailing_text, "x"}}, and{:error, :not_positive}.
Read from the first line down
- Read
{:ok, value}: Return successful data with a label. - Read
with ... <- ... do: Continue while each pattern matches. - Read
else: Turn failed matches into explicit errors.
Symbols are not secret signs
{:ok, value}Return successful data with a label.
with ... <- ... doContinue while each pattern matches.
elseTurn failed matches into explicit errors.
Match each name to its meaning
result contract
A stable API returns the same outer shapes, such as {:ok, value} and {:error, reason}.
with
with chains successful matches and sends the first failed match to else.
bang function
A name ending in ! commonly returns a value or raises, while its partner returns tagged results.
Why these forms are useful
Run the example first. Predict one result, then change one input and run it again.
A stable API returns the same outer shapes, such as {:ok, value} and {:error, reason}.
with chains successful matches and sends the first failed match to else.
Why this shape?
Why return {:ok, value} or {ok, Value} so often?
Expected failure stays visible as ordinary data. A caller can list success, known failure, and fallback branches with patterns.
Unexpected failures that cannot be handled here may still raise and reach a supervision boundary. Turning every exception into a generic error discards evidence.
Coming from Java, Python, or JavaScript
Java
- Familiar starting point
- Checked or unchecked exceptions, plus Optional.
- What BEAM changes
- Tagged results keep expected business outcomes in normal control flow.
- False friend
- Not every error should crash, and not every exception should be caught here.
Python
- Familiar starting point
- Exceptions and
None. - What BEAM changes
- A tag names the failure class instead of asking one empty value to carry every meaning.
- False friend
- Translate third-party exceptions into a stable result at a boundary.
JavaScript
- Familiar starting point
- throw, rejected Promises, or null.
- What BEAM changes
- A tuple is not a Promise. It is data that has already arrived.
- False friend
- Read contract as acceptance criteria, not an asynchronous Promise.
Close the answer and try
Complete a with expression that accepts only an even integer.
with {number, ""} <- Integer.parse("12"), true <- ____(number, 2) == 0 do
{:ok, number}
else
_ -> {:error, :not_even_integer}
endTarget result: Return {:ok, 12}.
Stuck? Read one hint
Use rem/2.
After you run it, see one answer
with {number, ""} <- Integer.parse("12"), true <- rem(number, 2) == 0 do
{:ok, number}
else
_ -> {:error, :not_even_integer}
endThink about it: Why keep the outer tuple shapes stable?
Callers can pattern-match success and failure without guessing which type came back.
Remember these three lines
- 1
Return stable tagged shapes.
- 2
Use
withfor a short happy path. - 3
Reserve bang functions for APIs that intentionally raise.