Skip to content

The language

Errors: Option and Result

Brine has no exceptions. A value that may be missing is an Option[T], an operation that may fail returns a Result[T, E], and x? passes a None or Err straight back to the caller. Panics are for bugs, not for failures you expect.

Both are generic enums that every program has without an import:

enum Option[T]:
Some(value: T)
None
enum Result[T, E]:
Ok(value: T)
Err(error: E)

Their variants are written on their own: Some(3), None, Ok(x), Err("bad input"). Handle them with match, which makes you deal with both cases:

fn find(names: [str], target: str) -> Option[int]:
for i, name in enumerate(names):
if name == target:
return Some(i)
return None
fn parse_bit(s: str) -> Result[int, str]:
if s == "0":
return Ok(0)
if s == "1":
return Ok(1)
return Err(f"not a bit: {s}")
fn main():
let names = ["ann", "bob"]
match find(names, "bob"):
case Some(i):
print(f"bob is at {i}")
case None:
print("no bob")
match parse_bit("x"):
case Ok(bit):
print("bit", bit)
case Err(e):
print("failed:", e)
Output
bob is at 1
failed: not a bit: x

The error type E can be anything: a str message, an int code, or your own enum of failure kinds.

None on its own doesn’t say which Option it is. Where the context doesn’t either, annotate: let n: Option[int] = None (otherwise E0332).

fn parse_bit(s: str) -> Result[int, str]:
if s == "0":
return Ok(0)
if s == "1":
return Ok(1)
return Err(f"not a bit: {s}")
fn main():
let good = parse_bit("1")
let bad = parse_bit("7")
print(good.is_ok(), good.is_err(), bad.is_err())
print(good.unwrap(), bad.unwrap_or(-1))
print(good.ok(), bad.ok())
let some: Option[str] = Some("x")
let none: Option[str] = None
print(some.is_some(), none.is_none())
print(some.unwrap(), none.unwrap_or("default"))
Output
true false true
1 -1
Some(1) None
true true
x default
Method On Returns
is_some(), is_none() Option[T] bool
is_ok(), is_err() Result[T, E] bool
unwrap() both the value inside Some/Ok; panics on None/Err
unwrap_or(default) both the value inside, or default
ok() Result[T, E] Option[T]: Some(value) or None (drops the error)

That’s the complete list. To get at an error’s value, or to turn an Option into a Result, use match.

unwrap() is for when a None or Err would be a bug. If it happens, the program stops:

unwrap.br
fn main():
let port: Option[int] = None
print(port.unwrap())
Output
panic: unwrap() called on None
--> unwrap.br:3:11

x? unwraps an Option or Result: it gives the value inside Some/Ok, or returns the None/Err from the current function at once.

fn parse_bit(s: str) -> Result[int, str]:
if s == "0":
return Ok(0)
if s == "1":
return Ok(1)
return Err(f"not a bit: {s}")
fn parse_bits(text: str) -> Result[int, str]:
var n = 0
for c in text:
n = n * 2 + parse_bit(c)? # an Err is returned to the caller here
return Ok(n)
fn first_even(xs: [int]) -> Option[int]:
for x in xs:
if x % 2 == 0:
return Some(x)
return None
fn half_first_even(xs: [int]) -> Option[int]:
let e = first_even(xs)? # None is returned here
return Some(e / 2)
fn main():
print(parse_bits("101"), parse_bits("1x1"))
print(half_first_even([3, 8]), half_first_even([1, 3]))
Output
Ok(5) Err("not a bit: x")
Some(4) None

The rules:

  • ? on an Option needs the function to return an Option[...].
  • ? on a Result[T, E] needs the function to return a Result[..., E] with the same error type E. Errors are never converted for you.
  • ? can appear anywhere in an expression, as in n * 2 + parse_bit(c)?.

Breaking a rule is E0341:

fn first_even(xs: [int]) -> Option[int]:
for x in xs:
if x % 2 == 0:
return Some(x)
return None
fn main():
let e = first_even([1, 2])?
print(e)
error[E0341]: `?` needs the function to return Option[...]
--> main.br:8:13
|
8 | let e = first_even([1, 2])?
| ^^^^^^^^^^^^^^^^^^^ this returns from a function that returns `()`
|
= help: change the return type to `Option[...]`, or handle the value with `match`
= run `brine explain E0341` for more about this error
error: could not compile due to 1 error
fn parse_bit(s: str) -> Result[int, str]:
if s == "1":
return Ok(1)
return Err(f"not a bit: {s}")
fn checked(s: str) -> Result[int, int]:
let b = parse_bit(s)?
return Ok(b)
fn main():
print(checked("1"))
error[E0341]: `?` would return a `str` error from a function whose errors are `int`
--> main.br:7:13
|
7 | let b = parse_bit(s)?
| ^^^^^^^^^^^^^ the error here is `str`
|
= help: convert it with `match`, or make the function's error type match
= run `brine explain E0341` for more about this error
error: could not compile due to 1 error

Convert with match where the types differ. To go from a Result to an Option, use .ok() and then ?:

fn parse_bit(s: str) -> Result[int, str]:
if s == "1":
return Ok(1)
return Err(f"not a bit: {s}")
fn code_of(s: str) -> Result[int, int]:
match parse_bit(s):
case Ok(b):
return Ok(b)
case Err(_):
return Err(2)
fn bit_or_none(s: str) -> Option[int]:
let b = parse_bit(s).ok()? # Result to Option, then `?`
return Some(b)
fn main():
print(code_of("1"), code_of("x"), bit_or_none("1"), bit_or_none("x"))
Output
Ok(1) Err(2) Some(1) None

main may return Result[(), E], which lets it use ?. An Err that reaches the end of main prints error: and the error to stderr, and the program exits with code 1.

The file and process built-ins never crash on failure; they return a Result (or an Option or bool) and the failure is a value you handle:

Built-in Returns
read_file(path) Result[str, str]: the text (must be UTF-8), or the reason it couldn’t be read
write_file(path, text) Result[(), str]
file_exists(path) bool
getenv(name) Option[str]
run(cmd) Result[int, str]: Ok(exit code), or Err if it can’t start

Together with ? and a main that returns Result, a program reads like the happy path and still handles every failure:

lines.br
fn count_lines(path: str) -> Result[int, str]:
let text = read_file(path)?
return Ok(len(text.lines()))
fn main() -> Result[(), str]:
write_file("notes.txt", "one\ntwo\nthree\n")?
print(count_lines("notes.txt")?)
match count_lines("missing.txt"):
case Ok(n):
print(n)
case Err(e):
print("skipped:", e)
print(count_lines("settings.txt")?)
print("not reached")
return Ok(())
Output (exit code 1)
3
skipped: cannot read missing.txt: No such file or directory
error: cannot read settings.txt: No such file or directory

The os module’s functions work the same way; see the os module and built-ins.

Use Option and Result for failures a correct program must expect: missing files, bad input, a key that isn’t there. Use a panic for a bug: a state that should be impossible.

A panic stops the program with a message, the source location, and exit code 101. You can’t catch one. Panics come from:

  • the language’s own checks: integer overflow, division by zero, an out-of-bounds index, an out-of-range conversion (see values and types);
  • unwrap() on None or Err, and parsing bad text with int(s) or float(s);
  • assert cond, which checks at run time and also tells the prover the condition holds afterwards (see contracts);
  • panic(msg), which you call yourself.
average.br
fn average(xs: [float]) -> float:
if len(xs) == 0:
panic("average of an empty array")
return sum(x for x in xs) / float(len(xs))
fn main():
print(average([1.0, 2.0]))
let none: [float] = []
print(average(none))
Output
1.5
panic: average of an empty array
--> average.br:3:9

Better still, turn the bug into something the compiler checks: requires len(xs) > 0 makes every caller prove the array isn’t empty, at compile time (see contracts).

  • Option[T] is Some(value) or None; Result[T, E] is Ok(value) or Err(error). Both are always available.
  • Methods: is_some, is_none, is_ok, is_err, unwrap (panics), unwrap_or, ok. Everything else is match.
  • x? returns None/Err early; the function must return Option[...], or Result[..., E] with the same E (E0341).
  • main may return Result[(), E]: an Err prints error: ... and exits with 1.
  • File and process built-ins return failures as values.
  • Panics (exit 101) are for bugs and can’t be caught.