W# 0.1.1

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_int(half(10) orelse -1);
    print_int(half(7) orelse -1);

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

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

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

    // The error propagated by `try` surfaces here.
    print_int(average(3, 7, 2) catch 0);
    print_int(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 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.

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.

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 8 September 2026. Improve this page

On this page