Skip to content
You are reading next, which describes the unreleased compiler. Khora is unstable before v1: syntax, standard-library APIs and behavior may change. The current release is documented under v0.4.

Control flow

Khora’s control-flow forms are expressions or block-like expressions. This page records their accepted shapes and result rules.

{
let value = compute();
value + 1
}

General shape:

{
Statement*
Expr?
}

The final expression, when present without a semicolon, is the block’s value. A block with no final value produces ().

if condition {
when_true
} else {
when_false
}

else if is nested if syntax:

if first {
a
} else if second {
b
} else {
c
}

The condition must be Bool. When an if is used as a value, its branches must agree on a result type. An if without else must produce ().

match value {
Pattern => Expr,
Pattern if guard => Expr,
}

An arm body may be a block:

match value {
Result::Ok(item) => {
log(item);
item
},
Result::Err(error) => fallback(error),
}

Patterns are checked for exhaustiveness and reachability. See Patterns.

match score {
value if value >= 90 => "high",
value if value >= 50 => "medium",
_ => "low",
}

The guard runs only after its pattern matches and must produce Bool.

while condition {
body;
}

The condition must be Bool.

for pattern in iterable {
body;
}

The left side is a pattern. Khora has a tuple literal — see Tuple and unit expressions — but no standard iterator yields one: an iterator over pairs yields a Pair record, so the loop binds it by name and reads its fields.

for entry in Dict::entries(table) {
print("${entry.key}: ${entry.value}");
}

Pair<K, V> has the fields key and value, and String::split_once answers a Split with the fields head and rest. Both are ordinary records: they are read with ., and written as record literals — let p: Pair<String, Int> = { key: "a", value: 1 };.

A tuple pattern — for (key, value) in ... — is only valid against a value whose type really is a tuple, and is refused against a Pair:

error: this pattern takes a value apart into 2 pieces, but `Pair<String, Int>`
is not a tuple

for desugars to Iterator::next, so both Iterator and Step must be in scope in the module that writes the loop:

import std::core::{Iterator, Step};

A tuple is taken apart by pattern and never by position: there is no pair.0. Destructure it in the for, in a let, or in a match arm.

let point = (3, 4);
let (x, y) = point;

Reach for a transform such as List::map when the result is another collection, and for for when the point is the body run once per element.

loop {
body;
}

A loop exits through break, return, raise, failure propagation, cancellation, or other non-local control flow.

Without a value:

break;

With a value:

break result;

A loop with value-carrying break expressions produces the common type of those values:

let found = loop {
let candidate = next();
if candidate.valid {
break candidate;
}
};
continue;

continue skips the remainder of the current loop iteration.

Return a value:

return value;

Return unit:

return;

A function normally returns its body’s final expression; return is the explicit early-exit form. Prefer the final expression where it stays clear, and return where an early exit is what makes the control flow readable.

Explicit raise:

raise UserError::NotFound(id)

Propagate a fallible call:

load_user(id)!

Handle a typed failure:

load_user(id)! catch {
UserError::NotFound(_) => User::guest(),
UserError::Unavailable(reason) => User::offline(reason),
}

raise is not return. It leaves through the function’s typed failure channel, which is a second exit the signature declares separately; ! propagates that channel and catch handles it before it reaches the caller. See Failures.

Postfix installation serves one expression:

load_user(id)! with {
store: test_store,
}

Block installation serves a lexical region:

with Production {
run_server()!
}

See Capabilities for capability rows and overrides.

Khora 0.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource