A transfer abandons the current expression and resumes at a surrounding control boundary. The form decides both the target and the value carried there.
| Form | Target | Payload |
|---|---|---|
return value |
enclosing function or closure | success value |
error value |
enclosing fallible function or closure | error value |
unsuccessful postfix ? |
enclosing compatible function or closure | None or Err(err) |
break value |
nearest or labeled loop | optional loop result |
continue |
nearest or labeled loop | none |
A transfer has no value at its own position, so it never constrains the type of a sibling branch.
Return
return evaluates its operand, leaves the current function or closure, runs
cleanup for every scope it exits, and hands the value to the caller.
fn classify(value: int): string {
if value < 0 {
return "negative"
}
if value == 0 {
return "zero"
}
return "positive"
}A bare return is valid when the success type is void:
fn run(ready: bool): void {
if !ready {
return
}
println("go")
}A tail expression or an expression-bodied function is clearer for a single final
value; use explicit return for early exits and when several completion paths
should be easy to compare.
fn double(value: int): int => value * 2return inside a closure returns from that closure invocation, not from the
function that created it:
fn clamp_all(values: []int): []int {
return values.map(value => {
if value < 0 {
return 0
}
value * 2
})
}The value must match the declared success type:
fn f(): int {
return "nope"
}Error
error value leaves through the function’s error channel.
error InputError { NotPositive { value: int }, TooLarge }
fn require_positive(value: int): int ! InputError {
if value <= 0 {
error NotPositive { value: value }
}
if value > 1000 {
error TooLarge
}
return value
}The variant name is enough when the function’s error type makes it unambiguous. The payload must belong to the declared error type or a compatible union:
error AuthError { Denied }
error StorageError { Full }
fn f(): int ! AuthError {
error Full
}error is a keyword throughout the language, so error payloads are bound as
err or e, never error.
Postfix ?
? is a conditional transfer. It unwraps Some/Ok, and on None/Err it
leaves through the enclosing boundary immediately.
error LoadError { NotFound }
fn load(id: int): int ! LoadError {
if id < 0 { error NotFound }
return id
}
fn total(a: int, b: int): int ! LoadError {
return load(a)? + load(b)?
}It works the same way for Option in an Option-returning function:
fn head(values: []int): int? {
first := values.get(0)?
return Some(first * 2)
}The boundary is lexical and must be compatible. ? on an Option inside a
function that returns a plain int has nowhere to go:
fn maybe(): int? { return None }
fn f(): int {
value := maybe()?
return value
}Break
break exits the nearest enclosing loop.
fn count_until_zero(values: []int): int {
mut seen := 0
for value in values {
if value == 0 {
break
}
seen += 1
}
return seen
}break value makes the loop produce a result. Every value-bearing break
targeting the same loop must agree on a type:
fn f(): int {
mut index := 0
result := for {
if index > 3 { break 1 }
break "two"
}
return result
}Because an iteration or condition loop can also finish through its head — a path
that contributes void — the bare for { } form is the clearest shape for a
value search. See Loops.
break outside any loop is rejected:
fn f(): int {
break
return 0
}Continue
continue ends the current iteration after running cleanup for the scopes it
leaves, then resumes at the loop head.
struct Item { id: int, valid: bool }
fn consume(items: []Item): int {
mut used := 0
for item in items {
if !item.valid {
continue
}
used += item.id
}
return used
}An iteration loop advances its iterator, a condition loop rechecks its condition, and a pattern-consume loop re-evaluates and re-matches its head.
Labels
A label names an enclosing loop so break and continue can target it past the
innermost one. The label is written 'name: at the loop and 'name at the use.
fn first_terminal(rows: [][]int): int {
mut visited := 0
'rows: for row in rows {
for cell in row {
if cell < 0 {
break 'rows
}
visited += 1
}
}
return visited
}continue 'label starts the next iteration of the labeled loop, leaving every
inner scope first:
fn count_clean(rows: [][]int): int {
mut clean := 0
'rows: for row in rows {
for cell in row {
if cell < 0 {
continue 'rows
}
}
clean += 1
}
return clean
}A label is not a character literal and not a general jump target — it must name an enclosing loop:
fn f(values: []int): int {
for value in values {
break 'nowhere
}
return 0
}Cleanup
No transfer bypasses lexical ownership. Every exit follows the same sequence:
- evaluate the transfer’s payload, if any;
- stop evaluating the rest of the expression;
- run defers and managed destruction for the exited scopes, innermost first;
- resume at the function or loop target.
error IoError { Denied }
struct File { id: int }
impl File {
fn open(path: string): File ! IoError {
if path.is_empty() { error Denied }
return File { id: path.len() }
}
fn read_line(self): string ! IoError { return "line" }
fn close(self): void { println("closed ${self.id}") }
}
fn read_first(path: string): string? ! IoError {
file := File.open(path)?
defer file.close()
line := file.read_line()?
if line.is_empty() {
return None
}
return Some(line)
}The file closes on the propagated ?, on the return None, and on the
return Some(line) — after the return value has been evaluated, so the return
expression can still read the resource it is about to release.
A transfer from a nested loop leaves fewer scopes than a function return does, so identify the exact target before reasoning about which cleanup runs.
Diagnostics
The compiler rejects:
breakorcontinueoutside a loop;- a label with no matching enclosing loop;
- a break payload incompatible with the target loop;
- a return value incompatible with the function’s success type;
- an error value incompatible with the function’s error type;
- postfix
?without a compatible optional or fallible boundary.
These are static target and type errors. Whether a given transfer is reached at runtime is ordinary control flow and is not analysed.
Putting it together
error SyncError {
Unreachable { host: string }
Rejected { code: int }
}
struct Batch { host: string, items: []int }
fn send(host: string, item: int): int ! SyncError {
if host.is_empty() { error Unreachable { host: host } }
if item < 0 { error Rejected { code: item } }
return item
}
fn note(message: string): void { println(message) }
fn sync(batches: []Batch, budget: int): int ! SyncError {
mut spent := 0
'batches: for batch in batches {
defer note("finished ${batch.host}")
if batch.items.is_empty() {
continue
}
for item in batch.items {
if spent >= budget {
break 'batches
}
if item == 0 {
continue
}
accepted := match send(batch.host, item) {
Ok(value) => value
Err(err) => continue 'batches
}
spent += accepted
}
}
if spent == 0 {
error Rejected { code: 0 }
}
return spent
}Five different exits share one function: continue skips an item, continue 'batches abandons a whole batch after a failure, break 'batches stops on
budget, error leaves through the error channel, and return completes it.
Every one of them runs the per-batch defer for each scope it leaves.