Postfix catch attaches match arms to the Err side of a Result. The Ok
payload passes through untouched; each arm decides what happens to one failure.
error LoadError {
NotFound { id: int }
PermissionDenied
Storage { message: string }
}
fn load_name(id: int): string ! LoadError {
if id < 0 { error NotFound { id: id } }
return "ada"
}
fn describe(id: int): string {
name := load_name(id) catch {
NotFound { id: _ } => return "unknown"
PermissionDenied => return "private"
Storage { message: _ } => return "unavailable"
}
return name
}
fn main(): void {
println(describe(1))
println(describe(-1))
}What an arm body means
This is the rule that governs every use of catch, and it is easy to guess
wrong: an arm body produces a replacement error, not a replacement success
value.
| Every arm… | Type of the catch expression |
|---|---|
| produces a value | Result[T, E2], where E2 is the arm body type |
diverges (return, error, break, continue) |
T — the plain success type |
So a catch whose arms produce values does not unwrap anything. It rewrites
the error channel and leaves you holding another Result:
error AuthError { InvalidToken, Expired }
error RequestError { Unauthorized }
fn authenticate(token: string): int ! AuthError { error Expired }
fn attempt(token: string): int ! RequestError {
// `converted` is Result[int, RequestError] — still two-channel.
converted := authenticate(token) catch {
InvalidToken => RequestError.Unauthorized
Expired => RequestError.Unauthorized
}
return converted?
}
fn main(): void {
token := "t"
println("${attempt(token).is_ok()}")
}A catch that only substitutes a fallback value is therefore a type error, not
a recovery:
error ConfigError { Missing, Invalid }
struct Config { retries: int }
fn read_config(): Config ! ConfigError { error Missing }
fn load(): Config {
// Arms produce `Config`, so this is Result[Config, Config].
config := read_config() catch {
Missing => Config { retries: 3 }
Invalid => Config { retries: 1 }
}
return config
}To recover to a plain value, make every arm leave the expression.
Recovery by diverging
When each arm returns, the catch yields the success type directly.
error SourceError { NotFound, Other }
fn source(): int ! SourceError { error Other }
fn call(): int {
// Every arm returns, so `value` is a plain `int`.
value := source() catch {
NotFound => return 0
_ => return -1
}
return value
}For a single fallback value, the Result methods are shorter than a diverging
catch:
error SourceError { Bad }
fn source(): int ! SourceError { error Bad }
fn call(): int {
return source().unwrap_or(0)
}
fn call_lazily(): int {
return source().unwrap_or_else(e => 0)
}
fn main(): void {
println("${call()} ${call_lazily()}")
}Reach for catch when different variants deserve different treatment, and for
unwrap_or / unwrap_or_else when they do not.
Conversion
The common use of catch is mapping one error type onto another at a layer
boundary. Add ? to convert and propagate in one expression.
error AuthError { InvalidToken, Expired }
error RequestError { Unauthorized, Internal }
fn authenticate(token: string): int ! AuthError {
if token.is_empty() { error InvalidToken }
return 7
}
fn current_user(token: string): int ! RequestError {
user := authenticate(token) catch {
InvalidToken => RequestError.Unauthorized
Expired => RequestError.Unauthorized
}?
return user
}
fn main(): void {
token := "tok"
println("${current_user(token).is_ok()}")
}Every normally completing arm must agree on one target error type. Variant
and ErrorType.Variant both name a variant; the qualified form is useful when
two error types share a variant name.
Direct error exits
An arm can use error to leave the enclosing fallible function immediately.
Because those arms diverge, the binding receives the success payload.
error SourceError { Bad, Other }
error Mapped { FromBad, FromOther }
fn source(): int ! SourceError { error Bad }
fn use_source(): int ! Mapped {
value := source() catch {
Bad => error FromBad
Other => error FromOther
}
return value
}
fn main(): void {
println("${use_source().is_ok()}")
}This reads well when the mapping is the whole point and no local recovery exists.
Patterns and guards
Catch arms use the same patterns as match, including payload destructuring
and guards.
error BackendError { Backend { code: int } }
fn request(): int ! BackendError { error Backend { code: 409 } }
fn call(): int {
value := request() catch {
Backend { code } if code == 409 => return 0
Backend { code: _ } => return -1
}
return value
}Guards refine selection but do not remove a variant from the exhaustiveness set: several guarded arms for one variant still need an arm that accepts it unguarded.
Bind error payloads as err or e. error is a keyword and cannot be used as
an identifier anywhere in Atoll.
Exhaustiveness
Every reachable error variant must be covered, exactly as in match.
error SourceError { Bad, Other }
fn source(): int ! SourceError { error Bad }
fn call(): int {
value := source() catch {
Bad => return 0
}
return value
}That program is rejected with ATOLL2010: non-exhaustive match on SourceError — missing: Other. Add the missing variant, or use _ when treating the
remaining failures uniformly is intentional.
Catch versus match
Use catch when the success path should pass through unchanged and only the
error side needs attention. Use match when both channels need explicit
handling.
error LoadError { NotFound }
fn load_user(id: int): string ! LoadError { error NotFound }
fn show(id: int): string {
match load_user(id) {
Ok(user) => user
Err(e) => "error"
}
}The catch subject must be a Result, including the T ! E signature
spelling. catch is not an optional fallback operator — for Option, use
??, a pattern, or the Option methods:
fn first_or_zero(values: []int): int {
return values.get(0) ?? 0
}Evaluation
The subject is evaluated once. On Ok, the payload passes through and no arm
runs. On Err, arms are tried in source order using ordinary match selection;
guards may run until one succeeds, and only the selected arm body runs. Keep
guards free of effects whose repetition or order would be surprising.
Cleanup registered before the subject runs still obeys the enclosing scope’s
exit rules. A direct error or return from an arm is an early exit and runs
the applicable defers.
catch transforms a typed Err value. It does not catch runtime traps, host
termination, or arbitrary exceptions.