Errors
Functions that can fail, fail, try, converting errors with From, matching ok and err, unwrapping with !, and their C.
A function that can fail says what it fails with, after !. Its errors are values that the caller must handle where it makes the call. Calx has no exceptions, and a panic is for bugs (Printing and panics).
enum ParseError { Empty TooLong(usize)}
fn parse_name(raw: own String) -> String ! ParseError { if raw.is_empty() { fail ParseError.Empty } if raw.len() > 16 { fail ParseError.TooLong(raw.len()) } return raw}
fn greet(raw: own String) -> String ! ParseError { let name = try parse_name(raw) // passes the error up return "hi {name}"}
fn main() { for raw in ["steve", "", "a name far too long to fit"] { match greet(String(raw)) { ok(text) => println(text), err(ParseError.Empty) => println("no name"), err(TooLong(n)) => println("{n} letters is too long"), } }}hi steve no name 26 letters is too long
Failing
Section titled “Failing”-> T ! E means the function returns a T or fails with an E. fn f() ! E returns nothing, but can fail.
return vgives back the value, andfail ereturns the errore.- A function that returns nothing gives back success when it reaches its end, as it does without
!. - The error can be any type but
(). An enum is the usual choice, since the caller can match each case. AStringsuits an error that’s only a message. - A
pubfunction can fail only with a type other modules can name, as for what it returns. - A C function can’t fail. Declare what it returns, and check that in Calx.
fn check(health: i32) ! String { if health < 0 { fail "health can't be {health}" }}
fn main() { if let err(message) = check(-3) { println(message) }}health can't be -3
Handling an error
Section titled “Handling an error”The call to a function that can fail gives a result, T ! E: either ok with the value, or err with the error. A result is handled at the call, in one of these ways:
| Written | Does |
|---|---|
try f() |
f’s value. If f fails, the function returns the error, converted if its type is different |
match f() { ok(v) => ..., err(e) => ... } |
Runs the arm that matches. Every case must be covered |
if let ok(v) = f() { } |
Runs the block if f succeeds. if let err(e) = f() runs it if f fails |
let ok(v) = f() else { return } |
Binds the value, or runs the else, which must leave |
f()! |
f’s value. Panics if f fails |
An unsafe block that holds only the call passes its result through, so try unsafe { f() } and match unsafe { f() } { ... } handle it at the call too (libraries).
A result can’t be put in a variable, passed to a function or returned. _ = can’t discard it either. So an error never slips by unseen, and the code always says what happens to it (Strictness).
fn parse(n: i32) -> i32 ! String { if n < 0 { fail "negative" } return n}
fn main() { let r = parse(3)error: parse can fail, so handle its error here println("{r}")}try f() is f’s value when f succeeds. When it fails, the function returns that error at once, and everything it owns is dropped, as a return there would drop it. The function fails with the same error type as f, or one that converts from it. try can go anywhere a value can, even inside another call:
struct Reader { bytes: List<u8>, at: usize,}
impl Reader { fn byte(self: mut Reader) -> u8 ! String { if self.at >= self.bytes.len() { fail "ran out at byte {self.at}" } let b = self.bytes[self.at] self.at += 1 return b }
// A protocol VarInt: 7 bits a byte, low bits first. fn varint(self: mut Reader) -> i32 ! String { var value = 0 var shift = 0 loop { let b = try self.byte() value |= (i32(b) & 0x7f) << shift if b & 0x80 == 0 { return value } shift += 7 if shift >= 35 { fail "VarInt too long" } } }}
fn sum(r: mut Reader) -> i32 ! String { return (try r.varint()) + (try r.varint())}
fn main() { var r = Reader { bytes: [0xac, 0x02, 0x05, 0x01], at: 0 } println("{sum(mut r)!}") match sum(mut r) { ok(n) => println("{n}"), err(e) => println(e), }}305 ran out at byte 4
try in a function that can’t fail is an error, since there’s nowhere to pass the error to. Handle it with match there.
Converting errors
Section titled “Converting errors”A function that calls code with several error types fails with one of its own, usually an enum with a variant for each. try converts the call’s error to the function’s through From, which the function’s error type implements once for each type it converts from:
enum IoError { Closed,}
enum CodecError { TooLong(i32),}
enum NetError { Io(IoError), Codec(CodecError),}
impl From<IoError> for NetError { fn from(value: own IoError) -> Self { return NetError.Io(value) }}
impl From<CodecError> for NetError { fn from(value: own CodecError) -> Self { return NetError.Codec(value) }}
fn read(open: bool) -> i32 ! IoError { if not open { fail IoError.Closed } return 300}
fn check(n: i32) -> i32 ! CodecError { if n > 255 { fail CodecError.TooLong(n) } return n}
fn packet(open: bool) -> i32 ! NetError { let n = try read(open) // an `IoError` becomes a `NetError` return try check(n) // and so does a `CodecError`}
fn main() { match packet(false) { ok(n) => println("{n}"), err(e) => println("{e}"), } match packet(true) { ok(n) => println("{n}"), err(e) => println("{e}"), }}Io(Closed) Codec(TooLong(300))
From<T>is built in, likeEqual, so there’s nothing to import, and no module can declare anotherFrom. Its one function isfn from(value: own T) -> Self, and it’s a generic interface:From<IoError>andFrom<CodecError>are separate interfaces, so a type can implement both.- The impl goes in the error type’s module, since
Fromis built in.tryconverts with it in any module. fromtakes the error asown, so an error that owns memory, such as aString, moves into the new one, with no copy.NetError.from(e)converts by hand, and the argument’s type chooses the impl.- A type has no
Fromfor itself, and writing one is an error:trypasses an error of the function’s own type up as it is. - A call whose error has no conversion is an error that says which
Fromto implement:
enum IoError { Closed,}
enum NetError { Timeout,}
fn read() -> i32 ! IoError { fail IoError.Closed}
fn packet() -> i32 ! NetError { return try read()error: try passes the error up, but this call fails with an IoError and packet fails with a NetError; to convert one to the other, implement From<IoError> for NetError}
fn main() { println("{packet()!}")}Generic code converts through a bound: fn lift<E: From<IoError>>() -> i32 ! E can try a call that fails with an IoError.
Matching ok and err
Section titled “Matching ok and err”ok(p) and err(p) match a result, and p can be any pattern, such as err(ParseError.TooLong(n)) or ok(0). A match must cover both, and every case of each (Patterns). When the function returns nothing, ok holds nothing and is written alone:
fn save(slot: i32) ! String { if slot > 8 { fail "no slot {slot}" }}
fn main() { match save(2) { ok => println("saved"), err(e) => println(e), }}saved
ok(...) and err(...) aren’t values: a function gives back its value with return and its error with fail.
!
f()! is f’s value, and panics if f fails, as ! does on none. It suits errors that can’t happen in a correct program, and tests.
fn half(n: i32) -> i32 ! String { if n % 2 != 0 { fail "{n} is odd" } return n / 2}
fn main() { println("{half(8)!}") println("{half(3)!}") // panic: unwrapped an error with `!`}4
A main that fails
Section titled “A main that fails”fn main() ! E can pass errors up with try too. If it fails, the program writes error: and the error to standard error, after what it printed before, and exits with status 1. The error is shown with Display, so E must be a type that can be shown:
fn port(text: str) -> u16 ! String { let n = u16.parse(text) else { fail "{text} isn't a port" } return n}
fn main() ! String { let p = try port("25565") println("listening on {p}") _ = try port("lots") println("not reached")}// stderr: error: lots isn't a port// status: 1listening on 25565
main still takes no parameters, and returns nothing when it succeeds.
In a parallel for
Section titled “In a parallel for”Every iteration of a parallel for runs, so its body can’t fail or pass an error up with try. Each iteration keeps its own outcome instead, in its own element:
fn parse(n: i32) -> i32 ! String { if n < 0 { fail "negative" } return n * 2}
fn main() { let xs: List<i32> = [1, -2, 3] var doubled = [0; 3] var bad = [false; 3] parallel for k in 0usize..xs.len() { match parse(xs[k]) { ok(v) => doubled[k] = v, err(_) => bad[k] = true, } } println("{doubled[2]} {bad[1]}")}6 true
A result is a C struct, like an enum with data: a one-byte tag, 0 for ok and 1 for err, and a union holding the value or the error. A small one, such as u8 ! ReadError, is two bytes, so it comes back in a register. try is a branch that’s marked unlikely. It holds the error’s return, after the drops a return there would make. For example:
enum ReadError { End}
fn byte(bytes: List<u8>, at: usize) -> u8 ! ReadError { if at >= bytes.len() { fail ReadError.End } return bytes[at]}
fn short(bytes: List<u8>) -> i32 ! ReadError { let hi = try byte(bytes, 0) let lo = try byte(bytes, 1) return (i32(hi) << 8) | i32(lo)}
fn main() { let bytes: List<u8> = [1, 2] println("{short(bytes)!}")}258
becomes, with #line directives left out:
struct cx_Result_u8_ReadError { cx_u8 tag; union { struct { cx_u8 f0; } v0; struct { game__ReadError f0; } v1; } u; };
static cx_Result_i32_ReadError game__short(const cx_list_u8* restrict bytes) { CX_VIEW(bytes); cx_u8 hi = ({ cx_Result_u8_ReadError cx_t1 = game__byte(&(*bytes), ((cx_usize)0ULL)); if (__builtin_expect(cx_t1.tag != ((cx_u8)0u), 0)) { cx_Result_i32_ReadError cx_t2 = ((cx_Result_i32_ReadError){ .tag = ((cx_u8)1u), .u.v1 = { .f0 = cx_t1.u.v1.f0 } }); return cx_t2; } cx_t1.u.v0.f0; }); cx_u8 lo = ({ cx_Result_u8_ReadError cx_t3 = game__byte(&(*bytes), ((cx_usize)1ULL)); if (__builtin_expect(cx_t3.tag != ((cx_u8)0u), 0)) { cx_Result_i32_ReadError cx_t4 = ((cx_Result_i32_ReadError){ .tag = ((cx_u8)1u), .u.v1 = { .f0 = cx_t3.u.v1.f0 } }); return cx_t4; } cx_t3.u.v0.f0; }); return ((cx_Result_i32_ReadError){ .tag = ((cx_u8)0u), .u.v0 = { .f0 = ((cx_i32)(((cx_i32)((cx_u32)((cx_i32)hi) << 8)) | ((cx_i32)lo))) } });}When an earlier part of the same expression made a value that’s owned, such as the name.copy() in pair(name.copy(), try read()), the value waits in a temporary of the statement. The try is worked out before the call, and the temporary is cleared only as the value moves into it. So passing the error up drops the value, and the normal path doesn’t drop it twice, whatever order C evaluates the arguments in.
A try that converts calls the impl’s from on the error, in the unlikely branch, before the drops: .u.v1 = { .f0 = game__NetError__From_IoError__from(cx_t1.u.v1.f0) }. The impl’s C name includes the interface’s arguments, so a type’s impls of From don’t meet. In a release build, a small from, such as one that wraps the error in a variant, is inlined, so the conversion costs no call.
Still to build, in the spec: ! E in function types.