Errors and optionals
Volt has no exceptions. A function that can fail says so in its type and returns the error as a value; the caller decides what to do with it.
Error sets and E!T
Section titled “Error sets and E!T”An error set lists what can go wrong. E!T is either an error from E or a T.
use std::io;
error parse_error { EMPTY, BAD_DIGIT }
fn parse(s: str) -> parse_error!i32 { if (s.len == 0) { return parse_error::EMPTY; } var n = 0; for (ch) in s { if (ch < '0' || ch > '9') { return .BAD_DIGIT; // the set is known from the return type } n = n * 10 + (ch - '0') as i32; } return n;}
fn main() -> void { std::println("{} {} {}", parse("123"), parse(""), parse("1x"));}// expect: 123 error.EMPTY error.BAD_DIGITSuffixes after E!T apply to the whole error union: E!T& is a reference to one and E!T? an
optional one. The payload takes a suffix only in parentheses: E!(T*).
try and catch
Section titled “try and catch”try x gives x’s value, or returns its error from the current function (which must be able to
return it). x catch v gives v instead of an error; x catch |e| { ... } runs a block that
either leaves (return, break) or, labelled, gives a value (|e| :h { ... break :h v; }).
use std::io;
error parse_error { EMPTY, BAD_DIGIT }
fn parse(s: str) -> parse_error!i32 { if (s.len == 0) { return parse_error::EMPTY; } return 5;}
fn sum(a: str, b: str) -> parse_error!i32 { return try parse(a) + try parse(b);}
fn main() -> void { val x = sum("1", "") catch -1; val y = sum("1", "2") catch |e| { std::println("failed: {}", e); return; }; std::println("{} {}", x, y);}// expect: -1 10An error that reaches the end of main (fn main() -> !void) prints error: NAME and exits with 1
(see main).
Any error
Section titled “Any error”!T without a set is error!T, and error holds an error from any set. Every variant of every
error set has a code of its own in the program, so any error converts to error, keeping its code
(its name prints) but not a payload.
use std::io;
error net_error { TIMEOUT }error disk_error { FULL }
fn fetch(ok: bool) -> net_error!i32 { if (!ok) { return net_error::TIMEOUT; } return 1;}
fn save(ok: bool) -> disk_error!void { if (!ok) { return disk_error::FULL; }}
fn sync(a: bool, b: bool) -> !i32 { // can fail with TIMEOUT or FULL val n = try fetch(a); try save(b); return n;}
fn main() -> void { std::println("{} {} {}", sync(true, true), sync(false, true), sync(true, false));}// expect: 1 error.TIMEOUT error.FULLA match on an error tests variants of any set and needs a default arm. An error doesn’t go
back into a named set: a function returning net_error!i32 can’t try an !i32. Plain error
is an error of its own, for a function with nothing more to say: return error;.
Errors with data
Section titled “Errors with data”Variants of an error set can carry a payload, like enum variants, and error sets can be generic.
use std::io;
error config_error { MISSING: str, BAD_VALUE: (str, i32),}
fn port(n: i32) -> config_error!i32 { if (n > 65535) { return config_error::BAD_VALUE("port", n); } return n;}
fn main() -> void { std::println("{}", port(70000));}// expect: error.BAD_VALUE(port, 70000)defer and errdefer
Section titled “defer and errdefer”defer runs a statement when the scope ends, however it ends; errdefer only when it ends by
returning an error. They run in reverse order. (Deleting owned values is automatic: see
Ownership. defer is for everything else.)
use std::io;
error job_error { FAILED }
fn run(fail: bool) -> job_error!void { std::println("start"); defer std::println("cleanup"); errdefer std::println("rolled back"); if (fail) { return job_error::FAILED; } std::println("done");}
fn main() -> void { run(false) catch {}; run(true) catch {};}// expect: start// expect: done// expect: cleanup// expect: start// expect: rolled back// expect: cleanupLooking inside
Section titled “Looking inside”On a variable, .err is the error as an optional (E?: null when it holds a value), so if (r.err)
asks whether there’s one and if (val e = r.err) binds it. .value is the value; reading it when
there’s an error stops the program, in release builds too. Both see through a reference, as fields
do.
use std::io;
error e { NOPE }
fn get(ok: bool) -> e!i32 { if (ok) { return 4; } return e::NOPE;}
fn main() -> void { val bad = get(false); if (bad.err) { std::println("failed: {}", bad.err); } val good = get(true); if (good.err == null) { std::println("{}", good.value); }}// expect: failed: NOPE// expect: 4Optionals
Section titled “Optionals”T? is a T or null. x ?? fallback gives the value or the fallback, and the fallback can leave
instead: x ?? return, x ?? break, x ?? continue, x ?? @panic("..."). A { after ??
starts a struct or array literal (p ?? { x: 0, y: 0 }), not a block: a fallback that needs
statements goes in if (x) { ... } else { ... }, where x is the value inside the first block.
A fallback that owns something (x ?? name, a local std::string) moves only when x is empty;
otherwise it stays where it is and its scope deletes it. Either way name counts as moved after
it, as after an if that moves it in one branch. A field or element as the fallback is copied with
copy, as for any value taken from inside another.
use std::io;
fn index_of(xs: i32[..], want: i32) -> usize? { for (x, i) in xs { if (x == want) { return i; } } return null;}
fn main() -> void { val data: i32[] = { 4, 8, 15 }; val a = index_of(data[..], 8); val b = index_of(data[..], 16) ?? 99; std::println("{} {} {}", a, b, index_of(data[..], 3));}// expect: 1 99 nullNarrowing
Section titled “Narrowing”if (x) checks that an optional holds a value, and inside the branch x is that value, with no
unwrapping. while (x) narrows the same way.
use std::io;
fn next(n: i32) -> i32? { if (n > 0) { return n - 1; } return null;}
fn main() -> void { var cur: i32? = 3; var total = 0; while (cur) { total += cur; // cur is an i32 here cur = next(cur); } val maybe: i32? = 41; if (maybe) { std::println("{} {}", maybe + 1, total); }}// expect: 42 6x.value and x.none read an optional directly; when x is a variable or a field, x.value is
its value in place (&x.value is a reference to it). A condition is always a bool, except that
an optional is allowed (it means “has a value”), and so is a pointer (“isn’t null”).
Pointers (T*) may be null too; if (p) and p ?? x turn one into a reference. See
References and pointers.
Binding in if and while
Section titled “Binding in if and while”if (val v = e) { ... } runs its block with v bound to e’s value when e, an optional or an
error union, has one; the else runs when it doesn’t. On an error union, else |err| { ... }
binds the error. while (val x = e) loops for as long as e has a value, so an iterator’s
next() drives it. var instead of val gives a copy to change. The binding exists only in the
first block, and an owned value moves into it.
use std::io;use std::text;
fn lookup(id: i32) -> std::string? { if (id == 7) { return std::string::from("seven"); } return null;}
struct countdown { n: i32;}
attach fn next(this: countdown&) -> i32? { if (this.n == 0) { return null; } this.n -= 1; return this.n;}
fn main() -> void { if (val name = lookup(7)) { std::println("found {}", name.as_str()); } else { std::println("no one"); } if (val n = "4x".parse_int()) { std::println("{}", n); } else |err| { std::println("not a number: {}", err); } var c: countdown = { n: 3 }; while (val i = c.next()) { std::println("{}", i); }}// expect: found seven// expect: not a number: INVALID// expect: 2// expect: 1// expect: 0