Skip to content
GitHub
ReferenceErrors

Errors

Functions that can fail, fail, try, converting errors with From, matching ok and err, unwrapping with !, and their C.

All 11 examples tested by CIEdit this pageReport a problem

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).

Tested
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"),
}
}
}

-> T ! E means the function returns a T or fails with an E. fn f() ! E returns nothing, but can fail.

  • return v gives back the value, and fail e returns the error e.
  • 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. A String suits an error that’s only a message.
  • A pub function 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.
Tested
fn check(health: i32) ! String {
if health < 0 {
fail "health can't be {health}"
}
}
fn main() {
if let err(message) = check(-3) {
println(message)
}
}

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).

Doesn't compile, on purpose
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:

Tested
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),
}
}

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.

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:

Tested
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}"),
}
}
  • From<T> is built in, like Equal, so there’s nothing to import, and no module can declare another From. Its one function is fn from(value: own T) -> Self, and it’s a generic interface: From<IoError> and From<CodecError> are separate interfaces, so a type can implement both.
  • The impl goes in the error type’s module, since From is built in. try converts with it in any module.
  • from takes the error as own, so an error that owns memory, such as a String, 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 From for itself, and writing one is an error: try passes 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 From to implement:
Doesn't compile, on purpose
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.

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:

Tested
fn save(slot: i32) ! String {
if slot > 8 {
fail "no slot {slot}"
}
}
fn main() {
match save(2) {
ok => println("saved"),
err(e) => println(e),
}
}

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.

Tested
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 `!`
}

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:

Tested
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: 1

main still takes no parameters, and returns nothing when it succeeds.

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:

Tested
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]}")
}

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:

Tested
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)!}")
}

becomes, with #line directives left out:

C
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.