Errors as values
Capa has no exceptions. Failure is just a value of a specific type, and the compiler makes sure you handle it. Two sum types in the standard library do the work, plus one operator that makes them pleasant to use.
Option: "might not be there"
You met Option<T> implicitly in chapter 5: xs.get(i) returns one. The shape is the obvious one:
type Option<T> =
Some(T)
None
It is the standard library's way of saying "this function might not have a sensible value to return". Searching a list, looking up a key, parsing a string that might be malformed: all return Option.
fun main(stdio: Stdio)
match parse_int("42")
Some(n) ->
stdio.println("parsed: ${n}")
None ->
stdio.println("not a number")
The compiler will not let you reach into an Option without dealing with both cases. Pattern matching is the unflinching way; the methods on Option are the convenient way:
opt.is_some()/opt.is_none()· boolean checks.opt.unwrap_or(default)· the value, or a fallback.opt.map(f)· if Some, applyfto the inner value.opt.and_then(f)· if Some, callfwith the inner value;fitself returns anOption, useful for chaining.opt.or_else(f)· if None, callf()to produce a fallbackOption.
Result: "operation might fail with a reason"
Where Option says "value or no value", Result<T, E> says "value of type T on success, value of type E on failure". The failure carries information.
type Result<T, E> =
Ok(T)
Err(E)
Anything that does I/O or talks to the outside world returns a Result. Reading a file, fetching a URL, parsing a JSON document: each one returns Result<T, IoError> or similar.
fun main(stdio: Stdio, fs: Fs)
match fs.read_to_string("config.txt")
Ok(text) ->
stdio.println("file: ${text}")
Err(e) ->
stdio.eprintln("error: ${e}")
The methods mirror Option: r.is_ok(), r.is_err(), r.unwrap_or(default), r.map(f), r.map_err(g), r.and_then(f), r.or_else(f). Plus two projections: r.ok() turns a Result<T, E> into an Option<T> (dropping the error), and r.err() turns it into an Option<E> (dropping the value).
The ? operator
Writing match at every call site becomes tedious when half your function is "do thing, propagate failure, do next thing". Capa borrows Rust's ?: append it to an expression that returns Result<T, E> or Option<T> and it does the obvious thing.
fun load_config(fs: Fs) -> Result<String, IoError>
let text = fs.read_to_string("config.txt")?
let trimmed = text.trim()
return Ok(trimmed)
Reading this aloud: "let text be the result of read_to_string; if that returned Err, return it from load_config immediately; otherwise unwrap the Ok and bind text to the inner string". The single ? replaces a five-line match.
The same operator works on Option<T>, propagating None up:
fun first_two(xs: List<Int>) -> Option<Int>
let a = xs.first()?
let b = xs.get(1)?
return Some(a + b)
The function returns Option<Int>, so the ? on each line propagates None to the caller. The same rule applies for both: the enclosing function must return a matching Option / Result shape, otherwise the compiler rejects the ?.
Why no exceptions
The cost of unchecked exceptions: every function can fail in ways its signature does not declare, and the type system cannot help you enumerate the failure modes. The runtime answer is "wrap everything in try", which most code does not.
Capa's approach: failure becomes a value, the value carries a type, the compiler refuses to let you ignore it. The ? operator gives you the ergonomics of unchecked exceptions in the cases where you genuinely want to "fail through", and full pattern matching for the cases where you want to recover.
The one escape hatch is Unsafe (chapter 8 introduces capabilities; Unsafe is the most permissive). Python interop crosses an Unsafe boundary and brings Python's exception semantics back into the program; the manifest flags every such function.
Try this. Write fun safe_divide(a: Int, b: Int) -> Option<Int> that returns None when b is zero. Now write a caller that uses ? to chain two divisions and returns None if either fails.
The two error modes
First: ? in a function whose return type does not match. If the inner expression is Result<T, E>, the function must also return a Result<_, E>:
fun read_first_line(fs: Fs) -> String
let text = fs.read_to_string("config.txt")?
return text
error: '?' can only propagate to a function returning Result<_, E>
or Option<_>; this function returns String
2 | let text = fs.read_to_string("config.txt")?
^
Either change the return type to Result<String, IoError>, or stop using ? and pattern-match locally.
Second: forgetting that Option is not the inner type. Same shape we saw at the end of chapter 5:
fun main(stdio: Stdio)
let xs = [10, 20, 30]
let doubled = xs.first() * 2
error: '*': expected Int * Int or Float * Float,
got Option<Int> * Int
Pattern match on the Option, use .unwrap_or(default), or apply ? when the enclosing function returns a compatible type.
Where you are now
You can model failures as values, handle them with match, propagate them with ?. The next chapter is the heart of Capa: capabilities. We have been using Stdio all along; chapter 8 is where it stops being a magic word and starts being a tool you reason with.