Results, Options, and Error Handling
- Gleam has no exceptions: errors are ordinary values returned by functions.
- The
Resulttype carries either a success value (Ok) or a failure reason (Error).- Functions that return
Resultforce callers to acknowledge failure at compile time.
- Functions that return
- The
Optiontype represents a value that may or may not be present. - The
useexpression eliminates deeply nestedcaseblocks when chaining multiple fallible operations.
Errors as Values
- Python raises an exception when something goes wrong
and expects callers to catch it with
try/except - But nothing in the type signature tells you whether a function might raise or what kind of exception to expect
- In Gleam, a function that might fail returns a Result, and the type signature makes that explicit
Result(a, e)is a type with two variants:Ok(value)carries a successful resultError(reason)carries a failure description
- The type of
reasonis up to you- It can be a
String, a custom error type, or anything else
- It can be a
- A caller cannot use the value inside
Okwithout handling theErrorcase- The compiler enforces this
Parsing and Dividing Safely
fn parse_int(s: String) -> Result(Int, String) {
case int.parse(s) {
Ok(n) -> Ok(n)
Error(_) -> Error("not an integer: " <> s)
}
}
int.parse(s)from the standard library returnsResult(Int, Nil)- It returns
Ok(n)if the string is a valid integer - It returns
Error(Nil)if not; the error carries no information
- It returns
- The function wraps the result to provide a
Stringerror message instead - Compare to Python's
int(s)- Raises
ValueErroron bad input - But no way to know this from the function's type
- Raises
fn safe_divide(a: Int, b: Int) -> Result(Int, String) {
case b {
0 -> Error("division by zero")
_ -> Ok(a / b)
}
}
Ok(42)
Error("not an integer: not a number")
Ok(5)
Error("division by zero")
safe_dividereturnsError("division by zero")instead of crashing with aZeroDivisionError- The
case b { 0 -> … _ -> … }pattern is idiomatic for this kind of guard - Any caller that uses
safe_dividemust handle bothOkandError
Transforming Results
- Often want to apply a function to the value inside
Okwithout unwrapping it manually result.mapdoes exactly that.
io.println(string.inspect(ok_int(42) |> result.map(fn(x) { x + 1 })))
io.println(string.inspect(error_str("oops") |> result.map(fn(x) { x + 1 })))
Ok(43)
Error("oops")
result.map(r, f)appliesfto the value insideOk(v)and returnsOk(f(v))- If
risError(e),result.mapreturnsError(e)unchanged
- If
- This avoids a
caseexpression when you only care about the success path
The Option Type
Option(a)is a type with two variantsSome(value)wraps a present valueNonerepresents absence
fn option_inspect(opt: Option(Int)) -> String {
case opt {
Some(n) -> "got " <> int.to_string(n)
None -> "nothing"
}
}
Option(Int)is exactly like Python'sOptional[int]case opt { Some(n) -> ... None -> ... }is the standard way to unwrap it- The compiler will not let you use
noutside theSomearm
fn first_positive(nums: List(Int)) -> Option(Int) {
case nums {
[] -> None
[x, .._rest] if x > 0 -> Some(x)
[_, ..rest] -> first_positive(rest)
}
}
"got 42"
"nothing"
Some(99)
None
Some(3)
None
[x, .._rest] if x > 0combines a list pattern with a guard- If the head
xis positive, returnSome(x)immediately - Otherwise, recurse on the tail
- If the head
- An empty list or an all-negative list reaches
[] -> None - This is equivalent to Python's
next((x for x in lst if x > 0), None)but the return type makes the "might be absent" case explicit
Chaining with use
- When you have several operations that each return
Result, nestingcaseexpressions becomes unpleasant quickly useis syntactic sugar that flattens this nesting
let result = {
use x <- result.try(parse_int("10"))
use y <- result.try(parse_int("20"))
use z <- result.try(safe_divide(x + y, 3))
Ok(z * 2)
}
io.println(string.inspect(result))
use x <- result.try(parse_int("10"))is equivalent to:
case parse_int("10") {
Ok(x) -> ...rest of block...
Error(e) -> Error(e)
}
- Reading top to bottom: parse 10, parse 20, divide their sum by 3, double the result
- Each
useline binds a name from theOkvalue or short-circuits with theError - The final expression
Ok(z * 2)is the result of the whole block
let fail = {
use x <- result.try(parse_int("10"))
use y <- result.try(parse_int("bad"))
use z <- result.try(safe_divide(x + y, 3))
Ok(z * 2)
}
io.println(string.inspect(fail))
Ok(20)
Error("not an integer: bad")
parse_int("bad")returnsError("not an integer: bad")- The
useexpression short-circuits at that pointsafe_divideis never called- The whole block evaluates to
Error("not an integer: bad")
- Analogous to Python's
try/except, but the short-circuiting is explicit in the types rather than hidden in the runtime
Guidelines for Handling Errors
- Return
Resultwhenever a function might fail for an externally visible reason (bad input, missing file, network error) - Return
Optionwhen absence is the only failure mode - Use
let assert Ok(x) = ...only when failure truly cannot happen (like initialising a known-good constant at startup)- It panics on
Error
- It panics on
- Use
result.mapandresult.tryfor short transformations - Use
usefor chains of three or more fallible operations
Check Understanding
When should you use Option instead of Result?
-
Use
Optionwhen there is no useful error information to convey: a dictionary lookup either finds the key or it does not, and there is no meaningful "reason it failed". -
Use
Resultwhen you want to distinguish failure modes: parsing an integer from a string can fail because the string is empty, because it contains non-digit characters, or because it overflows. AResultlets you report which failure occurred.
How does the use expression compare to Python's try/except?
In Python,
try/except catches exceptions that are raised anywhere in the block,
regardless of whether the function signature mentions them.
Gleam's use only short-circuits when a function explicitly returns an Error variant.
This means you always know from the type whether a function can fail,
and the use block's short-circuiting is visible in the code structure.
The practical difference is that Python's exceptions are invisible in type signatures,
while Gleam's Result values are not.
Exercises
Safe division (5 minutes)
Write safe_divide(a: Int, b: Int) -> Result(Int, String)
that returns Error("division by zero") when b is 0
and Ok(a / b) otherwise.
Mapping over results (5 minutes)
Write a function that takes a List(String) and returns a List(Int)
containing only the elements that parse successfully as integers.
Use int.parse, list.filter_map, or a combination of list.map and list.filter.
Chaining with use (15 minutes)
Write three functions that each return Result(Int, String):
parse_age(s: String),
validate_age(n: Int) (returns Error if n < 0 or n > 150),
and to_birth_year(age: Int) that subtracts from the current year.
Chain them with use so that (for example)
"26" produces Ok(2000) and "-5" produces an error at the validation step.
First matching element (10 minutes)
Write find(lst: List(a), pred: fn(a) -> Bool) -> Option(a)
that returns Some(x) for the first element where pred(x) is True,
or None if no element matches.
Write it using recursion with a case expression.