Reference: Control Flow, Options, and Errors
Reference material. Result and ? are introduced in the arc on Parse into fields, fail loudly; this page is the full picture — if/match/loop/while/for, Perhaps<T>, and error propagation.
If, while, for-in, and loop
if/else, while, and for-in work the way they do in most languages, with two notable extras. if doubles as an expression — both branches must agree on a type — and allows a braceless body for a single expression:
fun clamp(value: i64, lo: i64, hi: i64) -> i64 {
if (value < lo) lo else if (value > hi) hi else value
}
fun main() {
println(clamp(15, 5, 10)); // 10
var sum := 0;
for (let i in 1..=10) { // ..= is inclusive; .. is exclusive
sum += i;
}
println("1 + 2 + … + 10 = ${sum}"); // 55
}
loop runs indefinitely; break expr exits it and produces a value, which is what makes loop useful for a search that needs to bail out early with a result:
fun first_over(threshold: i64, values: i64[]) -> Perhaps<i64> {
var i: u64 := 0;
loop {
if ((i as i64) >= values.len()) {
break None;
}
if (values[i] > threshold) {
break Some { value = values[i] };
}
i += 1;
}
}
fun main() {
let result := first_over(6, [3, 1, 7, 2, 9, 4]);
match (result) {
Some { value } => println("first over 6: ${value}"), // 7
None => println("none found"),
}
}
Enums
An enum is a fixed set of named alternatives. A variant may carry named fields:
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
Unit,
}
fun area(s: Shape) -> f64 {
match (s) {
Circle { radius } => 3.14159 * radius * radius,
Rect { w, h } => w * h,
Unit => 1.0,
}
}
fun main() {
println(area(Rect { w = 2.0, h = 3.0 })); // 6
}
match on an enum must cover every variant (or end in _). A bare variant name in
a pattern binds its fields (Circle { radius }); a no-field variant is written
Unit. The qualified form Shape::Circle { … } is accepted anywhere and is
required in an expression when the expected type doesn't pin the enum down.
Perhaps<T> and Result<T, E> (below) are ordinary enums — None / Some { value }
and Err { error } / Ok { value } are just their variants.
Perhaps<T>: optional values
Perhaps<T> represents a value that may or may not be present. It has two variants:
Perhaps::Some { value: T }— holds a valuePerhaps::None— holds nothing
Use it whenever a function can legitimately return nothing (a search that finds no result, a field that is not set):
fun find(haystack: String[], needle: String) -> Perhaps<i64> {
var i := 0;
for (let s in haystack) {
if (s == needle) {
return Some { value = i };
}
i += 1;
}
return None;
}
fun main() {
let fruits := ["apple", "banana", "cherry"];
match (find(fruits, "banana")) {
Some { value } => println("found at index ${value}"), // 1
None => println("not found"),
}
match (find(fruits, "mango")) {
Some { value } => println("found at index ${value}"),
None => println("not found"), // not found
}
}
.yolo() unwraps a Perhaps::Some and panics on Perhaps::None — use it only when you are certain the value is present:
fun main() {
let result := Perhaps::Some { value = 42 };
let n := result.yolo(); // panics if None
println(n); // 42
}
.yolo() Is For Proven Cases
If absence is a normal outcome, keep the match. yolo() is the “this must exist” escape hatch, not the default style.
Result<T, E>: recoverable errors
Result<T, E> represents either a successful value or an error:
Result::Ok { value: T }— successResult::Err { error: E }— failure
Define your error type and return Result from any function that can fail:
struct ParseError {
message: String,
}
fun parse_positive(s: String) -> Result<i64, ParseError> {
if (s == "") {
return Err { error = ParseError { message = "input is empty" } };
}
// In a real program, you'd parse the string here.
// For this example, accept only "42".
if (s == "42") {
return Ok { value = 42 };
}
return Err { error = ParseError { message = "not a valid positive integer: ${s}" } };
}
fun main() {
let inputs := ["42", "", "hello", "42"];
for (let input in inputs) {
match (parse_positive(input)) {
Ok { value } => println("ok: ${value}"),
Err { error } => println("error: ${error.message}"),
}
}
}
Output:
ok: 42
error: input is empty
error: not a valid positive integer: hello
ok: 42
The ? operator
Writing match on every result is verbose. ? propagates an error automatically: if the result is Err, it returns the error from the current function immediately; if it is Ok, it unwraps the value and continues.
struct ParseError {
message: String,
}
fun parse_positive(s: String) -> Result<i64, ParseError> {
if (s == "") {
return Err { error = ParseError { message = "empty input" } };
}
if (s == "42") { return Ok { value = 42 }; }
if (s == "7") { return Ok { value = 7 }; }
return Err { error = ParseError { message = "unrecognised: ${s}" } };
}
fun double_parse(s: String) -> Result<i64, ParseError> {
let n := parse_positive(s)?; // returns Err immediately if parse fails
return Ok { value = n * 2 };
}
fun main() {
match (double_parse("42")) {
Ok { value } => println("doubled: ${value}"), // 84
Err { error } => println("failed: ${error.message}"),
}
match (double_parse("nope")) {
Ok { value } => println("doubled: ${value}"),
Err { error } => println("failed: ${error.message}"), // unrecognised: nope
}
}
? can only be used inside a function whose return type is Result<_, E>. If the error types differ, the inner error type must implement From<InnerError> for the outer error type — see the next section.
Reach For ? After The Result Shape Is Stable
Start with an explicit match if the flow is still confusing. Once the success/error path is clear, replace the boilerplate with ?.
Error coercion with From
When a function calls multiple fallible helpers that return different error types, ? can unify them automatically via the From aspect from the Aspects tutorial. Define extend AppError: From<SomeOtherError> once, and every ? on a Result<_, SomeOtherError> inside a function returning Result<_, AppError> converts through it for free.
struct IoError { msg: String }
struct ParseError { msg: String }
struct AppError { msg: String }
extend AppError: From<IoError> {
fun from(value: IoError) -> AppError {
AppError { msg = "io: ${value.msg}" }
}
}
extend AppError: From<ParseError> {
fun from(value: ParseError) -> AppError {
AppError { msg = "parse: ${value.msg}" }
}
}
fun read_data(path: String) -> Result<String, IoError> {
if (path == "data.txt") { Ok { value = "42" } }
else { Err { error = IoError { msg = "file not found: ${path}" } } }
}
fun parse_number(s: String) -> Result<i64, ParseError> {
if (s == "42") { Ok { value = 42 } }
else { Err { error = ParseError { msg = "not a number: ${s}" } } }
}
// Both ? calls coerce their error type to AppError via From
fun load_and_parse(path: String) -> Result<i64, AppError> {
let raw := read_data(path)?;
let n := parse_number(raw)?;
Ok { value = n }
}
fun main() {
match (load_and_parse("data.txt")) {
Ok { value } => println("loaded: ${value}"), // loaded: 42
Err { error } => println("failed: ${error.msg}"),
}
match (load_and_parse("missing.txt")) {
Ok { value } => println("loaded: ${value}"),
Err { error } => println("failed: ${error.msg}"), // failed: io: file not found: missing.txt
}
}
The ? on read_data(path)? automatically calls AppError::from(io_error) because IoError and AppError differ, and extend AppError: From<IoError> exists. No explicit conversion needed.
Match with guards
A match arm can have a guard — a condition that must also be true for the arm to fire:
fun classify(n: i64) -> String {
match (n) {
0 => "zero",
n if n < 0 => "negative",
n if n % 2 == 0 => "positive even",
_ => "positive odd",
}
}
fun main() {
for (let n in [-3, 0, 4, 7]) {
println("${n}: ${classify(n)}");
}
}
Output:
-3: negative
0: zero
4: positive even
7: positive odd
match also destructures tuples directly, binding or ignoring each element by position:
fun describe(point: (i64, i64)) -> String {
match (point) {
(0, 0) => "origin",
(x, 0) => "on the x-axis at ${x}",
(0, y) => "on the y-axis at ${y}",
(x, y) => "at (${x}, ${y})",
}
}
A complete example: input validation pipeline
The following program chains several fallible steps and collects results into a summary:
struct ValidationError { field: String, reason: String }
fun validate_name(name: String) -> Result<String, ValidationError> {
if (name.len() == 0) {
return Err { error = ValidationError {
field = "name",
reason = "must not be empty",
}};
}
if (name.len() > 32) {
return Err { error = ValidationError {
field = "name",
reason = "must be 32 characters or fewer",
}};
}
Ok { value = name }
}
fun validate_age(raw: String) -> Result<i64, ValidationError> {
// Simplified: only accept a handful of values for this example.
match (raw) {
"17" => Err { error = ValidationError {
field = "age",
reason = "must be 18 or older",
}},
"25" => Ok { value = 25 },
"30" => Ok { value = 30 },
_ => Err { error = ValidationError {
field = "age",
reason = "unrecognised value",
}},
}
}
struct User {
name: String,
age: i64,
}
fun validate_user(name: String, age_str: String) -> Result<User, ValidationError> {
let valid_name := validate_name(name)?;
let valid_age := validate_age(age_str)?;
Ok { value = User { name = valid_name, age = valid_age } }
}
fun main() {
let attempts := [
("Ada", "25"),
("", "30"),
("Alan", "17"),
("Ada", "30"),
];
for (let attempt in attempts) {
let name := attempt.0;
let age := attempt.1;
match (validate_user(name, age)) {
Ok { value } =>
println("ok: ${value.name}, age ${value.age}"),
Err { error } =>
println("invalid ${error.field}: ${error.reason}"),
}
}
}
Output:
ok: Ada, age 25
invalid name: must not be empty
invalid age: must be 18 or older
ok: Ada, age 30
What you learned
if/elseworks as a statement and as an expression.whilerepeats while a condition holds;for-initerates over arrays and ranges.loop { break value; }produces a value when the right moment is found.Perhaps<T>represents an optional value; always handle bothSomeandNone.Result<T, E>represents a recoverable error;?propagates errors automatically.From<E>lets?coerce between error types without explicit conversions.matchguards (if cond) add extra conditions to individual arms; tuples destructure by position.