Skip to content
volt
GitHub

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.

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_DIGIT

Suffixes 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 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 10

An error that reaches the end of main (fn main() -> !void) prints error: NAME and exits with 1 (see main).

!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.FULL

A 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;.

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 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: cleanup

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: 4

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 null

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 6

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

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