W# 0.2.0

Optionals and errors

?T may be absent, !T may have failed, and !T says which failures it means.

?T is a value that may be absent. !T is one that may have failed. Both compile to a tag next to the payload, in registers, with no allocation and no boxing.

// Optionals and error unions.
//
// `?T` is a value that may be absent; `!T` is one that may have failed. Both
// compile to a tag next to the payload -- no allocation, no boxing.
//
// A plain value coerces into either when the context asks for it, which is what
// lets `return n;` sit in a function declared `!i64`.

fn half(n: i64) ?i64 {
    if (n % 2 != 0) { return null; }
    return n / 2;
}

fn checked_div(a: i64, b: i64) !i64 {
    if (b == 0) { return error.DivideByZero; }
    return a / b;
}

// `try` unwraps an error union or returns the error from this function, so
// this one has to be fallible too.
fn average(a: i64, b: i64, count: i64) !i64 {
    const total = a + b;
    const mean = try checked_div(total, count);
    return mean;
}

fn main() i64 {
    // `orelse` supplies a value for the null case.
    print(half(10) orelse -1);
    print(half(7) orelse -1);

    // `|v|` binds the payload when it is present.
    if (half(8)) |v| { print(v); } else { print("odd"); }
    if (half(9)) |v| { print(v); } else { print("odd"); }

    // `.?` asserts presence, and aborts if it is wrong.
    print(half(6).?);

    // `catch` supplies a value for the error case, optionally binding the error.
    print(checked_div(10, 2) catch 0);
    print(checked_div(10, 0) catch 0);
    print(checked_div(10, 0) catch |e| -1);

    // The error propagated by `try` surfaces here.
    print(average(3, 7, 2) catch 0);
    print(average(3, 7, 0) catch -99);
    return 0;
}
wsharp run examples/results.ws
5
-1
4
odd
3
5
0
-1
5
-99

Optionals

?i64 is either an i64 or null. Four things act on one:

a orelse bthe payload, or b if it is absent
if (a) |v| { }binds the payload when present, with an optional else
while (a) |v| { }the same, as a loop, which is how iterators end
a.?the payload, and a panic if there is not one

.? is an assertion. It is the right thing to write when absence would be a bug rather than a case, and it fails loudly rather than quietly: the process prints W# panic: unwrapped a null optional and exits 101.

Error unions carry a set

!i64 is either an i64 or an error. What makes it worth more than a boolean is that the type records which errors:

fn risky(n: i64) !i64 {            // inferred: fn(i64) !{Negative, Zero}i64
    if (n < 0) { return error.Negative; }
    if (n == 0) { return error.Zero; }
    return n;
}

The set is inferred from what the function raises and from what it propagates with try. You can also write it down, in which case it is checked:

fn risky(n: i64) !{Negative, Zero}i64 { ... }        // exactly these two
fn risky(n: i64) !{Negative}i64 { ... }              // error: Zero escapes

Raising an error outside a declared set is a compile error. Nothing caps how many errors a set may name.

Because the set is in the type, the e bound by catch |e| is worth testing against:

const v = risky(n) catch |e| if (e == error.Negative) 0 else -1;

Handling one

try f()the payload, or return this error from the current function
f() catch 0the payload, or 0
f() catch |e| ...the payload, or an expression with the error bound
f() catch |e| return ethe payload, or hand this error on
f() catch return falsecatch takes any expression, including a return
f() catch { log(); 0 }including a block, whose last expression is its value

try propagates, so a function that uses it has to be fallible itself. That is why average in the example is declared !i64: it calls checked_div with try, and checked_div can fail.

A caught error can be handed on. catch |e| ... return e is how a function handles one case and passes the rest along, and it is held to the same declared set try is. An error value and an !T share their first slot, so this is one move rather than a constructor.

Coercion into either

A plain value coerces into an optional or an error union when the context wants one, so return n; is legal in a function declared !i64 and in one declared ?i64. You never write a constructor.

The same rule covers subtyping: a subtype coerces into its supertype, because both are one pointer and a subtype’s layout begins with a byte-identical copy of its supertype’s. That is the next page.

The steps compose, so return Sub{ .. }; is legal in a function declared !Base, and !?T is a type worth writing: a value, nothing, or a failure, in one return.

What panics rather than returning an error

W# separates errors, which are values the type system makes you handle, from failures the type system permits but the program must not perform. The second kind panics: it prints W# panic: <reason> to stderr and exits 101.

Ordinary arithmetic overflow is not on that list. +, - and * wrap, which for an unsigned type is the definition rather than a concession.

Last changed 11 September 2026. Improve this page

On this page