Result[T, E] is the outcome of a fallible operation: either an Ok(T)
success or an Err(E) typed failure. In function signatures it is normally
spelled T ! E.
error ParseError { Empty, TooLong }
fn parse_tag(text: string): string ! ParseError {
if text.is_empty() { error Empty }
if text.len() > 16 { error TooLong }
return text.to_lower_ascii()
}
fn main(): void {
match parse_tag("Release") {
Ok(tag) => println("tag ${tag}")
Err(e) => println("rejected")
}
match parse_tag("") {
Ok(tag) => println("tag ${tag}")
Err(e) => println("rejected")
}
}fn parse_tag(text: string): string ! ParseError and
fn parse_tag(text: string): Result[string, ParseError] declare the same type.
The ! spelling is idiomatic in signatures; the explicit form is useful in
annotations and generic positions.
error ParseError { Empty }
fn parse(text: string): int ! ParseError {
if text.is_empty() { error Empty }
return text.len()
}
fn main(): void {
// The same type, written both ways.
a: Result[int, ParseError] = parse("abc")
b := parse("abc")
println("${a.is_ok()} ${b.is_ok()}")
// Constructed directly.
ok: Result[int, ParseError] = Ok(1)
bad: Result[int, ParseError] = Err(ParseError.Empty)
println("${ok.is_ok()} ${bad.is_err()}")
}Like Option, every Result method is total. There is no unwrap that can
abort:
error E { Bad }
fn load(): int ! E { error Bad }
fn main(): void {
n: int = load().unwrap()
println("${n}")
}Inspection
error HttpError { Timeout, NotFound, Server { status: int } }
fn fetch(path: string): int ! HttpError {
if path.is_empty() { error Timeout }
return 200
}
fn main(): void {
good := fetch("/health")
bad := fetch("")
println("${good.is_ok()} ${good.is_err()}")
println("${bad.is_ok()} ${bad.is_err()}")
// Predicates that only run on the matching side.
println("${good.is_ok_and(status => status < 300)}")
println("${bad.is_err_and(e => true)}")
}Inspection is right for a boolean decision. When a payload is needed, match
handles both channels at once, and error variants can be matched individually:
error HttpError { Timeout, NotFound }
fn fetch(path: string): int ! HttpError {
if path.is_empty() { error Timeout }
if path == "/missing" { error NotFound }
return 200
}
fn report(path: string): void {
match fetch(path) {
Ok(status) => println("${path} -> ${status}")
Err(HttpError.Timeout) => println("${path} timed out")
Err(HttpError.NotFound) => println("${path} is gone")
}
}
fn main(): void {
report("/health")
report("")
report("/missing")
}Bind the error payload as err or e. error is a keyword and can never be
used as an identifier.
Variants with named fields
A variant declared with named fields is raised with the field names and
matched positionally. The error statement takes Variant { field: value };
Err(...) and match patterns take Variant(binding).
error DiskError { Full { free: int }, Offline }
fn write(bytes: int): int ! DiskError {
if bytes > 100 { error Full { free: 100 } }
return bytes
}
fn main(): void {
match write(500) {
Ok(n) => println("wrote ${n}")
Err(DiskError.Full(free)) => println("only ${free} bytes free")
Err(DiskError.Offline) => println("offline")
}
}Writing Err(DiskError.Full { free: 100 }) does not work: at expression
position, DiskError.Full is a constructor function, and the following brace
is parsed as a separate record literal. Either raise it with error, or call
the constructor positionally as DiskError.Full(100).
error DiskError { Full { free: int } }
fn write(bytes: int): int ! DiskError {
return Err(DiskError.Full { free: 100 })
}
fn main(): void {
println("${write(500).is_err()}")
}Discharging a failure
unwrap_or supplies an eager fallback; unwrap_or_else receives the error and
computes one.
error CacheError { Miss, Corrupt }
fn read_cache(key: string): int ! CacheError {
if key.is_empty() { error Miss }
return key.len()
}
fn recover(e: CacheError): int {
return match e {
CacheError.Miss => 0
CacheError.Corrupt => -1
}
}
fn main(): void {
println("${read_cache("abc").unwrap_or(0)}")
println("${read_cache("").unwrap_or(0)}")
println("${read_cache("").unwrap_or_else(e => recover(e))}")
}Both forms discard the failure, so use them only where the fallback genuinely
finishes the job. Where the failure still matters, keep the Result:
| Method | On Ok |
On Err |
|---|---|---|
or(other) |
keep the original success | use other |
or_else(f) |
keep the original success | call f(err) |
and(other) |
continue with other |
keep the original error |
error SourceError { Unavailable }
fn primary(): int ! SourceError { error Unavailable }
fn replica(): int ! SourceError { return 7 }
fn main(): void {
// Fall back to the replica, still as a Result.
value: Result[int, SourceError] = primary().or(replica())
println("${value.unwrap_or(-1)}")
// Lazy: the replica is only consulted after a real failure.
lazy := primary().or_else(e => replica())
println("${lazy.unwrap_or(-1)}")
}Transformation
map rewrites the success channel, catch_err rewrites the error channel, and
flat_map chains another fallible step with the same error type.
error IoError { NotFound }
error ImportError { Unreadable, Invalid }
fn read_text(path: string): string ! IoError {
if path.is_empty() { error NotFound }
return "id=1"
}
fn decode(text: string): int ! ImportError {
if !text.starts_with("id=") { error Invalid }
return text.len()
}
fn import(path: string): int ! ImportError {
return read_text(path)
.catch_err(e => ImportError.Unreadable)
.flat_map(text => decode(text))?
}
fn main(): void {
println("${import("data.txt").unwrap_or(-1)}")
println("${import("").unwrap_or(-1)}")
}catch_err is the method form of the postfix catch
operator: both convert Result[T, E] into Result[T, F]. Use the method in a
pipeline and the operator when different variants deserve different treatment.
To remove one layer from a Result[Result[T, E], E], use flat_map with the
identity callback:
error E { Bad }
fn inner(): int ! E { return 1 }
fn main(): void {
nested: Result[Result[int, E], E] = Ok(inner())
flat: Result[int, E] = nested.flat_map(r => r)
println("${flat.is_ok()}")
}The prelude also declares flatten(), but it cannot do that job. Its signature
is fn flatten(self): Result[T, E], so on a Result[Result[int, E], E] the
parameter T is already Result[int, E] and the result keeps the same
nesting — and the method is bodyless besides, so calling it fails to build:
error E { Bad }
fn inner(): int ! E { return 1 }
fn main(): void {
nested: Result[Result[int, E], E] = Ok(inner())
flat: Result[int, E] = nested.flatten() // ATOLL2002: found Result[Result[int, E], E]
println("${flat.is_ok()}")
}If the inner and outer error types differ, map one channel with catch_err
before collapsing.
Conversion
ok() and err() each keep one channel and throw the other away.
error E { Bad }
fn step(i: int): int ! E {
if i == 1 { error Bad }
return i * 10
}
fn main(): void {
results := [step(0), step(1), step(2)]
// Keep only the successes.
values := results.filter(r => r.is_ok()).map(r => r.unwrap_or(0))
println("${values.len()} succeeded")
// Count the failures without inspecting them.
println("${results.count(r => r.is_err())} failed")
// Single-channel views.
first: int? = results.get(0).flat_map(r => r.ok())
println("${first.unwrap_or(-1)}")
println("${step(1).err().is_some()}")
}Those conversions are for an explicit boundary — a place where the caller has decided the failure detail is not needed. They are not a substitute for propagation.
Result also declares to_list() (“one success item, or an empty list”), but
it is a bodyless prelude declaration: it type-checks and then fails to build
with ATOLL2004. Option.to_list() does have a body, so r.ok().to_list() is
the working route.
Control flow
Most code should reach for the language constructs before the methods.
| Construct | Purpose |
|---|---|
value? |
unwrap Ok, or return the Err from the current function |
match value |
handle both channels locally |
value catch { ... } |
convert or discharge the error side |
error Variant |
leave the current fallible function with a failure |
error DbError { Closed }
error ApiError { Unavailable, Internal }
fn query(sql: string): int ! DbError {
if sql.is_empty() { error Closed }
return sql.len()
}
fn handler(sql: string): int ! ApiError {
// `?` propagates DbError after `catch` has converted it.
rows := query(sql) catch {
Closed => ApiError.Unavailable
}?
if rows > 1000 { error Internal }
return rows
}
fn main(): void {
println("${handler("select 1").unwrap_or(-1)}")
println("${handler("").unwrap_or(-1)}")
}? requires an enclosing fallible function. Using it in a void function is
rejected:
error E { Bad }
fn load(): int ! E { error Bad }
fn main(): void {
v := load()?
println("${v}")
}Collecting fallible steps is a ? loop — the first failure exits with the
error, and the caller sees a single Result:
error StepError { Failed { at: int } }
fn step(i: int): int ! StepError {
if i == 3 { error Failed { at: i } }
return i * i
}
fn run(n: int): []int ! StepError {
mut out: []int = []
mut i := 0
for i < n {
out.add(step(i)?)
i = i + 1
}
return out
}
fn main(): void {
empty: []int = []
println("${run(3).unwrap_or(empty).len()}") // 3
println("${run(5).unwrap_or(empty).len()}") // 0 — failed at i = 3
}Result and Option
The two types answer different questions: Option says “there is no value”,
Result says “the operation failed, and here is why”. Move between them at the
boundary where that distinction changes.
error LookupError { Unknown { id: int } }
fn find(id: int): string? {
if id == 1 { return Some("ada") }
return None
}
// Absence becomes a failure: the caller needs an explanation.
fn require(id: int): string ! LookupError {
unknown := LookupError.Unknown { id: id }
return find(id).to_result(unknown)?
}
// Failure becomes absence: the caller only wants the happy path.
fn maybe(id: int): string? {
return require(id).ok()
}
fn main(): void {
println("${maybe(1) ?? "<none>"}")
println("${maybe(2) ?? "<none>"}")
println("${require(2).is_err()}")
}Equality and text
to_string() renders the active variant with its payload, and hash_code()
mixes the variant with the payload’s hash, so results can appear inside derived
hashable values when their component types support it.
error E { Bad }
fn load(i: int): int ! E {
if i < 0 { error Bad }
return i
}
fn main(): void {
println("${load(1).to_string()}")
println("${load(-1).to_string()}")
}A worked example
A two-stage import that reads records, validates each one, converts the two underlying failure types into one API-level error, and reports how far it got.
error IoError { Missing { path: string } }
error ValidationError { EmptyName, BadAge { value: int } }
error ImportError { Unreadable, Rejected, Partial { at: int } }
struct Row { name: string, age: int }
struct Person { name: string, age: int }
fn read_rows(path: string): []Row ! IoError {
if path.is_empty() { error Missing { path: path } }
return [
Row { name: "ada", age: 36 },
Row { name: "grace", age: 45 },
Row { name: "", age: 20 },
]
}
fn validate(row: Row): Person ! ValidationError {
if row.name.is_empty() { error EmptyName }
if row.age < 0 { error BadAge { value: row.age } }
return Person { name: row.name, age: row.age }
}
fn import_people(path: string): []Person ! ImportError {
rows := read_rows(path) catch {
Missing { path: _ } => ImportError.Unreadable
}?
mut people: []Person = []
mut index := 0
for row in rows {
person := validate(row) catch {
EmptyName => { error Partial { at: index } }
BadAge { value: _ } => { error Rejected }
}
people.add(person)
index = index + 1
}
return people
}
fn describe(path: string): void {
match import_people(path) {
Ok(people) => println("${path}: imported ${people.len()}")
Err(ImportError.Unreadable) => println("${path}: could not be read")
Err(ImportError.Rejected) => println("${path}: rejected")
Err(ImportError.Partial(at)) => println("${path}: stopped at row ${at}")
}
}
fn main(): void {
describe("people.csv")
describe("")
}import_people never lets IoError or ValidationError escape: each is
converted at the point it arrives, so the signature []Person ! ImportError
is the complete contract its callers have to handle. See
Errors for the full propagation rules.