Elixir basics · Lesson 18 (open contents)
01 · Let the code speak02 · Meet six kinds of values03 · Put values together04 · Build a new list05 · Make the shapes match06 · Choose with case07 · Parse a number safely08 · Unpack &1 and the pipe09 · Put code in a project10 · Ask precise true-or-false questions11 · Read and update nested data12 · Count visible text and bytes13 · Let function clauses choose14 · Solve a list twice15 · Choose the smallest clear control form16 · Give domain data a shape17 · Put one tested module in Mix18 · Make success and failure predictable19 · Read and write one real file20 · Take only the data you need21 · Give different data one shared action22 · Write a callback contract23 · Write down the data contract24 · Test behavior and boundaries25 · Turn a project into a command26 · Write the project promise27 · Parse one real line28 · Keep the public API small29 · Make the promise executable30 · Package the edge and leave clues31 · Prove the project is done
ELIXIR · INTERMEDIATE · LESSON 1825 minutes

Make success and failure predictable

Use tagged tuples, `with`, and a small error contract.

01 · Start with the whole example

Run this first

Run it once, change one input, and compare the new result.

Elixir
# 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")}
Check the result first
  • The results are {:ok, 7}, {:error, {:trailing_text, "x"}}, and {:error, :not_positive}.
02 · Take the code apart

Read from the first line down

  1. Read {:ok, value}: Return successful data with a label.
  2. Read with ... <- ... do: Continue while each pattern matches.
  3. Read else: Turn failed matches into explicit errors.
03 · Meet the new symbols

Symbols are not secret signs

{:ok, value}

Return successful data with a label.

with ... <- ... do

Continue while each pattern matches.

else

Turn failed matches into explicit errors.

04 · Ideas inside the code

Match each name to its meaning

01

result contract

A stable API returns the same outer shapes, such as {:ok, value} and {:error, reason}.

02

with

with chains successful matches and sends the first failed match to else.

03

bang function

A name ending in ! commonly returns a value or raises, while its partner returns tagged results.

05 · Make the idea clear

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.

DESIGN DECISION

Why this shape?

Why return {:ok, value} or {ok, Value} so often?

Choice

Expected failure stays visible as ordinary data. A caller can list success, known failure, and fallback branches with patterns.

Cost and boundary

Unexpected failures that cannot be handled here may still raise and reach a supervision boundary. Turning every exception into a generic error discards evidence.

A FAMILIAR POINT OF VIEW

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.
06 · Change it yourself

Close the answer and try

Complete a with expression that accepts only an even integer.

Practice starting point
with {number, ""} <- Integer.parse("12"), true <- ____(number, 2) == 0 do
  {:ok, number}
else
  _ -> {:error, :not_even_integer}
end

Target result: Return {:ok, 12}.

Stuck? Read one hint

Use rem/2.

After you run it, see one answer
One answer
with {number, ""} <- Integer.parse("12"), true <- rem(number, 2) == 0 do
  {:ok, number}
else
  _ -> {:error, :not_even_integer}
end
Think about it: Why keep the outer tuple shapes stable?

Callers can pattern-match success and failure without guessing which type came back.

Take with you

Remember these three lines

  1. 1

    Return stable tagged shapes.

  2. 2

    Use with for a short happy path.

  3. 3

    Reserve bang functions for APIs that intentionally raise.

Lesson completeRun the code once, then mark this lesson as done.