Error patterns
An error as an enum#
Task. Describe several reasons for refusal with one type and handle them in one match.
@error enum PayError {
Declined(reason: str)
Fraud
LimitExceeded(max: i64)
}
fn charge(amount: i64) -> i64 | PayError {
if amount > 1000 { fail PayError.LimitExceeded(1000) }
if amount == 13 { fail PayError.Fraud }
if amount < 0 { fail PayError.Declined("negative amount") }
amount
}
fn describe(r: i64 | PayError) -> str =>
match r {
i64 n => "charged {n}"
PayError.Declined(reason) => "declined: {reason}" // variants of an error enum are matched like any others
PayError.Fraud => "fraud suspected"
PayError.LimitExceeded(max) => "over the limit of {max}"
}
fn main() io {
for a in [50, 13, -5, 5000] { print(describe(charge(a))) }
}Output
charged 50 fraud suspected declined: negative amount over the limit of 1000
@error enum PayError { Declined(reason: str); Fraud; LimitExceeded(max: i64) }is one "family" of errors; the variants carry data.- The function returns
i64 | PayError, andmatchtakes apart both success and every variant. A forgotten variant isnon-exhaustive. - When to use an
enumand when several@error data: anenumif the caller always handles all reasons together; separate types if different callers care about different errors and the union grows along the call chain.
Retry with a delay#
Task. Repeat an unstable operation several times without repeating forever.
@error data Flaky { attempt: i32 }
fn unreliable(attempt: i32) -> str | Flaky {
if attempt < 3 { fail Flaky{ attempt } }
"ok on attempt {attempt}"
}
fn withRetry(max: i32) -> str | Flaky io, time {
var attempt = 1
loop {
match unreliable(attempt) {
str s => break s
Flaky e => {
if attempt >= max { fail e } // out of attempts: return the last error
print("attempt {attempt} failed, retrying")
sleep(Duration.ofMillis(10 * attempt as i64)) // a linear delay between attempts
attempt += 1
}
}
}
}
fn main() io, time {
match withRetry(5) {
str s => print(s)
Flaky e => print("gave up after {e.attempt}")
}
match withRetry(2) {
str s => print(s)
Flaky e => print("gave up after {e.attempt}")
}
}Output
attempt 1 failed, retrying attempt 2 failed, retrying ok on attempt 3 attempt 1 failed, retrying gave up after 2
loop { … break value }is a loop expression:break syields the successful value,fail ethe last error when the attempts run out.- The delay grows with the attempt number (
10 * attemptms); for network rpc the same is done declaratively with the attributes@retry(n, backoff: …)- but only for@idempotentand@readonlymethods, so that a retry does not perform an action twice. - Pitfall: retry only transient errors (
Timeout,Unavailable). A retry will not fix "invalid input".
A fallback path#
Task. If a value is not in the cache, take it from the database; if the database is down, return an error.
@error data CacheMiss { key: str }
@error data DbDown { why: str }
fn fromCache(key: str) -> str | CacheMiss {
if key == "a" { return "cached-a" }
fail CacheMiss{ key }
}
fn fromDb(key: str) -> str | DbDown {
if key == "c" { fail DbDown{ why: "connection refused" } }
"db-{key}"
}
fn load(key: str) -> str | DbDown {
await fromCache(key)
!> CacheMiss => fromDb(key)? // !> replaces the error with a fallback path
}
fn main() io {
for k in ["a", "b", "c"] {
match load(k) {
str s => print("{k}: {s}")
DbDown e => print("{k}: db down ({e.why})")
}
}
}Output
a: cached-a b: db-b c: db down (connection refused)
await fromCache(key) !> CacheMiss => fromDb(key)?- the!>handler fires only onCacheMiss; other errors (hereDbDown) go further down the chain.- The result type of
loadisstr | DbDown:CacheMisshas disappeared from it because it is handled. The compiler checks this. - Several handlers can be placed in a row:
!> A => … !> B => ….
Releasing resources: defer#
Task. Close a resource for certain, however the function ends.
fn process(name: str) -> str | Failed io {
print("open {name}")
defer print("close {name}") // defer runs on every way out of the function
if name == "bad" { fail Failed{} }
print("work {name}")
"done {name}"
}
@error data Failed {}
fn main() io {
for n in ["good", "bad"] {
match process(n) {
str s => print(s)
Failed => print("failed {n}")
}
}
}Output
open good work good close good done good open bad close bad failed bad
defer expressionruns on any exit from the function: normal, viafail, via?and even on a panic.- Several
defers run in reverse order (the last first), like a call stack. - Types with
@must_close(Server,WebSocket, …) require closing: the compiler reportsmust-closeif you forgotdefer x.close().
Back: cookbook.