Step 5. Errors
Why#
OxL has no exceptions. An exception is an invisible way out of a function: looking at a signature you cannot know what it
may "throw". Instead an error is an ordinary value and the possible errors are listed in the result type:
-> User | NotFound | Timeout. A type marked @error cannot be ignored: if the result of a function that may return an error
is simply discarded, the compiler stops the build.
Separately there are panics: they mean a bug (division by zero, out-of-bounds access, a broken contract) and end the current task; they are not caught and handled like an ordinary error (§6.6).
Example#
@error data NotFound { id: i64 } // @error marks a type as an error
@error data Invalid { why: str }
fn find(id: i64) -> str | NotFound { // the result is str or NotFound
if id == 1 { return "ann" }
fail NotFound{ id } // fail returns the error
}
fn parseId(s: str) -> i64 | Invalid {
match s.parseInt() {
i64 n => n
ParseError => fail Invalid{ why: "not a number: {s}" }
}
}
fn lookup(s: str) -> str | NotFound | Invalid { // the signature lists every error
let id = parseId(s)? // ? passes the error to the caller
find(id)?
}
fn main() io {
for s in ["1", "2", "x"] {
match lookup(s) { // match takes the result apart by type
str name => print("found {name}")
NotFound e => print("no user {e.id}")
Invalid e => print("bad input: {e.why}")
}
}
let r = await find(2) // a chain: -> is the next step, !> is a handler
-> it.len()
!> NotFound => 0
print("{r}")
}found ann no user 2 bad input: not a number: x 0
Walkthrough#
@error data NotFound { id: i64 }- an ordinary record marked as an error. Errors may be anenumas well.-> str | NotFound- "a string orNotFound".fail NotFound{ id }returns the error from the function; the error type must be part of the signature (otherwise -fail-type).parseId(s)?- the?operator: if the result is an error, the function returns it at once; otherwise the value comes out. The errors of both called functions are therefore listed in the signature oflookup.matchtakes the result apart by type. If you skip an error you getnon-exhaustive.->is a chain: the next step receives the result asit.!> NotFound => 0is a handler: the error is replaced by a value and disappears from the result type. Chains are especially useful for network calls and are described in §8.4.- If
mainreturns errors (fn main() -> () | Usage | IoError io), an unhandled error ends the process with exit code 1.
A few more ways to work with a result:
| Form | Meaning |
|---|---|
r? | return the error from the current function, otherwise extract the value |
r! | extract the value; on an error - panic (fine in main, tests, scripts) |
r is NotFound | a type test, narrows r in the if branch |
_ = r | explicitly discard the result (rarely and deliberately) |
x ?? d | a default value for T? |
Common mistake: an ignored result#
@error data Declined { reason: str }
fn charge(sum: i64) -> i64 | Declined => sum
fn main() {
charge(10) // a result carrying an @error cannot be ignored
}unhandled-error
This is the most frequent diagnostic of a beginner: unhandled-error. Fix it in one of these ways:
charge(10)? (pass it up), match, charge(10)! (if the error is impossible here), or _ = charge(10) (discard deliberately).
Remember#
- Errors are values in the result type, not exceptions.
?passes on,matchhandles,!>replaces in a chain.- The network adds
NetErrorto the result of every rpc (§6.5) - it is an ordinary error too, visible in the signature.
Next: collections and iterators.